SDK

NPM Version NPM Downloads

Start Here

This page explains how to integrate @froomle/frontend-sdk from first setup to advanced usage. It covers both declarative integrations (data-froomle-* placeholders) and programmatic integrations (JS/TS API calls and React bindings).

Use this page as a map:

Want working end-to-end examples instead of isolated snippets? See frontend-sdk-examples for full sample projects, including plain HTML, React, TypeScript, and ArcXP setups.

Choose an Integration Pattern

Mode Integration style Use when

Declarative

HTML attribute-centric (data-froomle-*)

You want SDK-driven DOM population from placeholders. Works for plain HTML and SSR templates (including JSP-generated markup).

Declarative

JS/TS bootstrap + HTML attributes

You initialize SDK in JS/TS (setEnvironment, setPageVisit, …​), while recommendation blocks are still mapped with data-froomle-*.

Programmatic

JS/TS API-centric

You fetch recommendations directly in code with getRecommendations or proxyReco and render your own UI.

Programmatic

Component-centric (React bindings)

You use @froomle/frontend-sdk/react (useCreateReco, FroomleReco, FroomleOrder, …​) to compose recommendation UI in React.

JSP + TS is the same integration model as JS/TS bootstrap + HTML attributes. The main difference is only where markup is rendered (server template vs client template generation).

Event Tracking by Implementation Choice

Implementation choice Auto event tracking What you need to do

Declarative (data-froomle-*)

Yes

The SDK init pipeline can emit page-level events (page_visit, detail_pageview) without any recommendation block. Automatic recommendation impression / click_on_recommendation tracking starts once rendered SDK-owned recommendation blocks exist, or once backend-rendered DOM keeps equivalent runtime metadata. See Advanced Hybrid Pattern.

React bindings (@froomle/frontend-sdk/react)

Yes (when using SDK React rendering primitives)

Mount FroomleSdkInit to enable page-level events first. Use SDK components/hooks for standard recommendation tracking behavior. For fully custom rendering, use the manual event helpers (sendAddToCart(…​), sendPurchase(…​), etc.) or the generic sendEvent(…​) fallback.

Programmatic (JS/TS API)

No

Initialize shared SDK state first, then send page/business events manually with the dedicated helper functions or sendEvent(…​), and only then add recommendation requests when ready. Follow Tracking events.

Advanced Hybrid Pattern: Backend-Fetched Recommendations + SDK Event Tracking

This is a supported advanced web integration pattern when:

  • your backend calls the Froomle recommendations API itself

  • the backend decides what recommendation content is rendered

  • the browser SDK still owns page-level event tracking, and where possible recommendation attribution

The contract is simple:

  1. Your backend requests recommendations from Froomle.

  2. Your backend passes the recommendation attribution metadata through to the rendered frontend markup.

  3. The browser initializes the SDK normally for page context (setEnvironment, setPageVisit, consent, and setContextItem on detail pages).

  4. The rendered recommendation nodes keep the Froomle runtime metadata the SDK needs for attribution.

Hybrid support matrix:

Frontend shape Can backend fetch recos? Automatic page events Automatic reco impression/click What you need to provide Support status

DOM / script-tag

Yes

Yes

Yes, when rendered nodes keep Froomle runtime metadata

Render recommendation nodes with data-froomle-reco, data-froomle-id, data-froomle-request-id, and a clickable <a>. Include data-froomle-item-type when available, and data-froomle-user-group when benchmark attribution matters.

Supported advanced pattern

React with FroomleReco

Prefer the normal SDK React request flow instead

Yes

Yes, for SDK-rendered React recos

Use FroomleSdkInit, useCreateReco(…​) / useRecoList(…​), and render items through FroomleReco.

Supported for normal SDK React rendering, not the primary backend-fetched hybrid contract

React custom render

Yes

Yes

No first-class guarantee

Treat recommendation attribution as manual unless you stay inside SDK-owned React rendering primitives.

Supported with manual reco tracking

Pure programmatic / manual

Yes

No

No

Send page/business events manually, and send recommendation attribution manually with list_name, request_id, and user_group when applicable.

Supported, fully manual

Required backend-to-frontend recommendation metadata:

  • placement list name as data-froomle-reco

  • returned item id as data-froomle-id

  • returned request id as data-froomle-request-id

  • returned item type as data-froomle-item-type when available

  • returned user_group as data-froomle-user-group when benchmarking or recommendation attribution needs it

If your backend strips request_id, list_name, or an applicable user_group before rendering, the browser SDK cannot preserve recommendation attribution automatically. In that case, fall back to manual recommendation events.

Example backend-rendered DOM:

<article
  class="headline-card"
  data-froomle-reco="home_recommended_for_you"
  data-froomle-id="article-123"
  data-froomle-request-id="req-456"
  data-froomle-item-type="article"
  data-froomle-user-group="froomle"
>
  <a href="/news/article-123">Headline title</a>
</article>

With that metadata in place, the DOM event layer can still recognize the node as a Froomle recommendation for impression and click_on_recommendation attribution.

Minimal mapping example:

type BackendRecoItem = {
  Id: string
  RequestId: string
  UserGroup?: string
  ItemType?: string
  title?: string
  uri?: string
}

function renderRecoCard(item: BackendRecoItem, listName: string): string {
  const attrs = [
    `data-froomle-reco="${listName}"`,
    `data-froomle-id="${item.Id}"`,
    `data-froomle-request-id="${item.RequestId}"`,
    `data-froomle-item-type="${item.ItemType ?? 'article'}"`,
  ]

  if (item.UserGroup) {
    attrs.push(`data-froomle-user-group="${item.UserGroup}"`)
  }

  return `
    <article class="headline-card" ${attrs.join(' ')}>
      <a href="${item.uri ?? '#'}">${item.title ?? item.Id}</a>
    </article>
  `
}

The important part is not the exact templating language. Whether you render in JSP, Twig, Blade, JSX on the server, or another SSR layer, the browser must still receive the same data-froomle-* attribution fields on the final DOM node.

Recommended Rollout Order

Assume item metadata integration is already in place outside the frontend SDK.

For production integrations, do not treat recommendation rendering as a substitute for event integration. Froomle models still rely on baseline event history, so the recommended rollout keeps event tracking live before recommendation placements are introduced.

Recommended order:

  1. Complete item metadata integration outside the frontend SDK.

  2. Initialize shared SDK state on every relevant page (setEnvironment, setPageVisit, consent, and setContextItem on detail pages).

  3. Enable page-level and business event tracking first.

  4. Verify page_visit, detail_pageview, and any manual retail/business events before enabling recommendations broadly.

  5. Choose the recommendation integration shape for each placement. If a placement is benchmarked, decide here whether it is standard rendering, branch-aware benchmark rendering, or control-aware benchmarking, and whether the SDK or the host application owns the control branch. See also When the SDK Can Fully Own the Benchmark.

  6. Add recommendation placements or programmatic recommendation requests.

  7. Verify recommendation attribution events (impression, click_on_recommendation) once recommendations are actually rendered.

By integration style:

  • Script-tag / declarative: SDK init can send page_visit or detail_pageview even before any recommendation placeholder is added. Automatic recommendation impression / click_on_recommendation tracking starts once recommendation blocks are rendered, including backend-rendered DOM that keeps equivalent runtime metadata. See Advanced Hybrid Pattern.

  • React: mount FroomleSdkInit on the page to enable page-level events first, then add FroomleReco, useCreateReco(…​), or useRecoList(…​) later. Automatic recommendation-item tracking still requires SDK-owned rendering or explicit manual tracking.

  • Programmatic JS/TS: initialize SDK state first, then send page/business events manually (sendEvent(…​), sendAddToCart(…​), sendRemoveFromCart(…​), sendPurchase(…​), …​), then add getRecommendations(…​) / proxy requests later.

  • Benchmarked recommendation placements: choose the benchmark shape when you implement the recommendation placement, not when you implement page/business events. That choice determines who owns control rendering, who resolves the branch in UI, and whether recommendation attribution can stay automatic.

Learning Path: Development vs Production

If you are new to the SDK, separate the two onboarding goals clearly:

Development path:

  1. Start with one script tag and one recommendation placeholder.

  2. Confirm a recommendation request reaches the browser network tab.

  3. Add detail-page context (context_item) for article/product pages.

Production rollout:

  1. Complete item metadata integration outside the frontend SDK.

  2. Initialize shared SDK state and enable page/business event tracking first.

  3. Add consent, identity, and histories.

  4. Choose the recommendation integration shape for each placement.

  5. If a placement is benchmarked, choose the benchmark shape at that moment as well. See When the SDK Can Fully Own the Benchmark and Branch-aware vs Control-aware Benchmarking.

  6. Add recommendation placements.

  7. Move to module-based setup (npm import), programmatic APIs, or React bindings when your frontend architecture requires them.

Development Flow (First Working Integration)

This is the shortest development path to see the SDK working in a browser. It is not the recommended production rollout order. For production onboarding, keep item integration separate and bring page/business event tracking live before recommendation placements. See Recommended Rollout Order.

Minimum concept:

  1. Configure environment and page visit.

  2. Add one recommendation placeholder with field mappings.

  3. Set consent.

  4. Verify /recommendations/requests in browser network logs.

If these four steps work, the rest of the SDK features are incremental extensions (filters, context item, histories, ordering, programmatic calls, React helpers).

Once you want a full runnable project instead of the minimal first integration, use frontend-sdk-examples.

Install and Run

Script-Tag Setup (No Build Tool)

Recommended starter (default defer mode):

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
  }, { once: true })
</script>
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>

Use froomle:before-init for settings that must be present on the first recommendation request.

Need a runnable example instead of the minimal snippet above? Use frontend-sdk-examples and start with the plain HTML examples.

Runtime Scope Guide

Use this quick map to choose the correct sections for your integration runtime:

Runtime Sections to use Notes

Script-tag/global (froomle.global.js)

Script-Tag Setup, Script Loading and Initialization Modes, Script-tag Whitelist

Uses HTML script attributes and init hooks (froomle:before-init, froomle:init).

Module/import (JS/TS bundles)

Module Setup, Core Configuration, Programmatic API (JS/TS)

Use imported setters (setEnvironment, setPageVisit, setConsent, …​). If you render declarative data-froomle-* placeholders from a module/import setup, call init() explicitly after those setters.

React bindings

Module Setup, React Bindings, Core Configuration

React apps use module/import setup. Script-tag init hooks do not apply.

Shared (all runtimes)

Core Configuration, Recommendations in Markup, Programmatic API (JS/TS)

Setting semantics are shared across runtimes. Only bootstrap/initialization entrypoint differs.

Shared Runtime State vs Recommendation Block Configuration

Across runtimes, distinguish between shared SDK runtime state and per-block or per-request configuration.

Common page/app-level runtime state is typically set once for the current page context:

  • setEnvironment(…​)

  • setPageVisit(…​)

  • setChannel(…​)

  • setConsent(…​)

  • setDeviceId(…​)

  • setUserId(…​)

  • setContextItem(…​)

  • setContextItemType(…​)

Optional runtime-wide overrides such as setRequestDomain(…​), setSafeRequest(…​), setRecommendationsBearer(…​), and setRecommendationsBearerProvider(…​) are also configured centrally when your integration requires them.

If a page contains multiple recommendation blocks, reuse that shared SDK state across all of them. If the page context changes in an SPA or hybrid navigation flow, update the page-level setters once for the new route/context.

Per-block or per-request configuration is usually supplied separately for each recommendation block or API call:

  • recommendation list name, limit, or list_size

  • block-specific filters and variables

  • recommendation request arguments passed to getRecommendations(…​), useCreateReco(…​), or useRecoList(…​)

  • block-specific rendering and layout

In script-tag integrations, shared runtime state usually lives on the SDK script tag or in froomle:before-init, while per-block configuration lives on the recommendation placeholders. In React and programmatic integrations, shared runtime state is typically initialized once in the app/bootstrap layer, while each recommendation block or API call provides its own list/filter/rendering configuration.

Script Loading and Initialization Modes (Global Script Tag)

Default for script-tag integrations: use defer.

This section applies to global/script-tag usage (froomle.global.js). For React and other module/import integrations, use module setup (setEnvironment, setPageVisit, setConsent) and ignore script loading modes.

Mode Use when Key requirement

defer (default)

Most websites and CMS templates.

Register first-request JS setters in froomle:before-init; use froomle:init or window.FroomleFrontendSdkReady for post-init code.

blocking

Legacy pages that require strict synchronous script order.

Keep the SDK tag without defer/async; if first-request values are set from JS, still register froomle:before-init before the SDK tag.

async

Startup order with other scripts is not strict.

Use froomle:before-init for first-request values and froomle:init/window.FroomleFrontendSdkReady for post-init logic.

Event-Driven Hook Pattern

For complex pages with multiple modules or microfrontends, use the SDK’s hook pattern on top of one of the real script loading modes above, typically defer or async:

  • froomle:before-init: pre-init settings that must affect the first automatic request.

  • froomle:init: post-init orchestration once initialization completes.

  • window.FroomleFrontendSdkReady: sticky fallback for late-loaded consumers.

This is an orchestration pattern, not a separate browser loading mode. Runtime diagnostics therefore keep reporting the underlying browser/script mode in diagnostics.lifecycle.inferredLoadingMode.

Hook Timing and Responsibilities

  • froomle:before-init: pre-init hook, fired before DOM init and before the first recommendation request.

  • froomle:init: post-init hook, fired once when SDK initialization completes.

  • window.FroomleFrontendSdkReady: sticky readiness promise for consumers that may attach after events already fired.

When to use each hook:

  1. Use froomle:before-init for settings that must be included in the first request: consent, custom variables, histories, dynamic channel, runtime identity, or recommendation auth.

  2. Use froomle:init for code that depends on completed initialization (module orchestration, UI hooks, diagnostics).

  3. Use window.FroomleFrontendSdkReady as a reliable fallback for late-loaded bundles/modules.

Diagnostics

For browser-side diagnostics and support reviews, the SDK exposes window.FroomleFrontendSdkRuntime as the single runtime source of truth.

window.FroomleFrontendSdkRuntime = {
  integration: {
    primaryMode: "dom",
    observedModes: {
      dom: true,
      framework: false,
      programmatic: false,
    },
    sources: ["dom:auto"],
    lastSource: "dom:auto",
  },
  init: {
    scope: "dom",
    trigger: "auto",
    status: "ready",
    error: null,
    ready: Promise,
  },
  diagnostics: {
    config: {
      sdkVersion: "0.4.0",
      environment: "lesoir",
      requestDomain: "publisher-api.example.com",
      requestOrigin: "https://publisher-api.example.com",
      pageType: "home",
      channel: "www-desktop",
      consent: 2,
      contextItem: null,
      contextItemType: null,
      hasUserId: true,
      hasDeviceId: true,
      deviceIdOwnership: "froomle",
      deviceIdSource: "sdk-generated",
      deviceIdContinuity: "stable-since-previous-page-load",
      deviceIdWarnings: [],
    },
    lifecycle: {
      beforeInitEventDispatched: true,
      initEventDispatched: true,
      inferredLoadingMode: "defer",
    },
    traffic: {
      lastRecommendationsRequestAt: 1710000000000,
      lastRecommendationsError: null,
      lastEventsRequestAt: 1710000000500,
      lastEventsError: null,
    },
    benchmark: {
      defaultConfig: {
        mode: "customer-managed",
        currentGroup: "customer",
        groups: {
          control: "customer",
          treatment: "froomle",
        },
      },
      lastRequestConfig: {
        mode: "customer-managed",
        currentGroup: "customer",
        groups: {
          control: "customer",
          treatment: "froomle",
        },
      },
      lastRequestListNames: ["homepage_headline"],
      lastResolvedRequestId: "123456789",
      lastResolvedUserGroup: "customer",
      lastResolvedBranch: "control",
      lastResolutionSource: "response",
      lastPlacementSurface: "react",
      lastPlacementVariant: "control-aware",
      lastPlacementRenderStrategy: "wait",
      lastUsedReturnedRecommendationContent: false,
      observedVariants: {
        responseDriven: false,
        controlAware: true,
      },
      warnings: [],
    },
    smartSorting: {
      lastResolvedAt: 1710000000300,
      lastSurface: "react",
      lastListName: "search_results_rerank",
      lastCandidateCount: 12,
      lastFixedRankCount: 1,
      lastResponseCount: 12,
      lastUnmatchedCount: 0,
      warnings: [],
    },
    autoExclusions: {
      enabled: true,
      effectiveConfig: {
        scope: "page",
        includePlacement: false,
        sources: [
          {
            selector: "[data-froomle-id]",
            idAttribute: "data-froomle-id",
            itemTypeAttribute: "data-froomle-item-type",
            itemType: "article",
            visibleOnly: true,
          },
        ],
      },
      lastBatchingAt: 1710000000200,
      lastBatchingSurface: "react",
      lastBatchingPlacementCount: 2,
      lastBatchingOutgoingRequestCount: 2,
      lastBatchingExclusionSignatureCount: 2,
      lastBatchingSplitByExclusions: true,
      lastCollectionAt: 1710000000250,
      lastCollectionSurface: "react",
      lastCollectedCount: 3,
      lastSkippedCurrentPlacementCount: 1,
      lastSkippedInvisibleCount: 0,
      lastMissingIdCount: 0,
      lastSourceCounts: {
        selector: 3,
      },
      lastSentCount: 3,
      lastSentOriginCounts: {
        selector: 3,
      },
      lastSentExclusions: [
        {
          id: "article-123",
          item_type: "article",
          reconsumable: false,
          origin: "selector",
          source: "[data-froomle-id]",
          origins: [
            { origin: "selector", source: "[data-froomle-id]" },
          ],
        },
      ],
      warnings: [],
    },
  },
}

Use these fields as follows:

  • integration.primaryMode: runtime owner for the page when the SDK can resolve one (dom, framework, or programmatic).

  • integration.observedModes: observed browser-side usage flags for dom, framework, and programmatic.

  • integration.sources: ordered unique list of observed runtime sources such as dom:auto, framework:react, or programmatic:sendPurchase.

  • integration.lastSource: latest observed source, useful when debugging mixed environments.

  • init.scope: init owner scope. For the current browser DOM lifecycle this is dom.

  • init.status: DOM/script-tag init lifecycle (idle, before-init, initializing, ready, failed).

  • init.trigger: whether DOM init started as auto or manual.

  • init.error: last DOM/script-tag init error when init.status is failed; otherwise null.

  • init.ready: sticky readiness promise for DOM/script-tag init.

  • diagnostics.config: support-facing effective SDK state for the current page, including sdkVersion, environment, request routing, page context, consent, and identity presence.

  • diagnostics.config.sdkVersion: SDK runtime version reported by the current browser bundle, useful when checking whether a rollout actually loaded the expected release.

  • diagnostics.config.environment, pageType, channel, consent, contextItem, and contextItemType: effective page/event/recommendation context after script attributes, init options, and runtime setters have been applied.

  • diagnostics.config.hasUserId and hasDeviceId: boolean identity-presence flags. They do not expose the actual user_id or device_id.

  • diagnostics.config.requestDomain: effective request host configured through setRequestDomain(…​) or script-tag data-froomle-request-domain. This is useful when debugging custom-domain, proxy, and CNAME browser integrations. It stays null when the current integration never configured an explicit request host.

  • diagnostics.config.requestOrigin: effective browser request origin derived from requestDomain and the current transport mode. When setSafeRequest(false) is active it uses http://…​;; otherwise it uses https://…​;. It stays null when requestDomain is null.

  • diagnostics.config.deviceIdOwnership: best-known identity provenance for the current browser device_id:

    • froomle: generated or established by an SDK-owned path.

    • customer: explicitly supplied by your integration, for example through setDeviceId(…​) or data-froomle-device-id.

    • unknown: a device_id exists, but the SDK cannot confidently establish ownership, for example a legacy cookie without provenance metadata.

  • diagnostics.config.deviceIdSource: most recent observed path for the current device_id, for example sdk-generated, setDeviceId, data-froomle-device-id, froomle_device_id-cookie, or recommendations-response.

  • diagnostics.config.deviceIdContinuity: cross-page/cross-tab continuity classification for the current browser device_id, for example no-previous-state, stable-since-previous-page-load, changed-since-previous-page-load, fresh-identified-after-anonymous-interval, anonymous-after-identified-state, or anonymous.

  • diagnostics.config.deviceIdWarnings: support-facing anomaly flags such as empty-string-hash, changed-during-page-lifecycle, or customer-id-before-consent-source-ready.

  • diagnostics.lifecycle: support-facing lifecycle observations separate from the core init contract. This includes whether froomle:before-init and froomle:init were dispatched and the inferred browser loading mode (blocking, defer, async, or custom).

  • diagnostics.traffic: timestamps of the last recommendation/event request attempt and the last stringified error summary, when available.

  • diagnostics.benchmark.defaultConfig: the current default benchmark config from setBenchmark(…​) or script-tag data-froomle-benchmark-* inputs. This is a convenience default, not a page-wide truth about one active user_group.

  • diagnostics.benchmark.lastRequestConfig: the last effective benchmark config that actually shaped a benchmarked recommendation request.

  • diagnostics.benchmark.lastRequestListNames: the last benchmarked list names seen on the wire.

  • diagnostics.benchmark.lastResolvedRequestId, lastResolvedUserGroup, lastResolvedBranch, lastResolutionSource: support-facing summary of the most recent benchmarked request/placement resolution.

  • diagnostics.benchmark.lastPlacementSurface, lastPlacementVariant, and lastPlacementRenderStrategy: where the last benchmarked placement was resolved (dom or react), which diagnostic variant was observed (response-driven means benchmark config without SDK-owned control content; control-aware means SDK-owned control content was provided), and which rendering strategy the SDK used for that placement. Current benchmark rendering uses wait, which hides the placement until the branch is resolved to avoid control-then-treatment flicker.

  • diagnostics.benchmark.lastUsedReturnedRecommendationContent: whether the most recent benchmarked control branch used returned Froomle content instead of customer/control content. With the current strict branch contract this should stay false; returned Froomle content is ignored on the customer/control branch unless a future explicit fallback option is introduced.

  • diagnostics.benchmark.observedVariants: cumulative flags for whether this runtime has already exercised branch-aware benchmark requests without SDK-owned control content (responseDriven) and/or SDK-owned control-aware benchmark behavior (controlAware).

  • diagnostics.benchmark.warnings: recent benchmark-specific dev/support warnings, for example missing/unusable control content on a customer/control branch, or unusable React raw control items that were skipped.

  • diagnostics.smartSorting: support-facing state for smart sorting and reranking. It reports the last resolved surface (dom, programmatic, or react), list name, candidate count, fixed-rank count, response count, unmatched candidate count, and recent smart-sorting warnings.

  • diagnostics.autoExclusions: support-facing state for opt-in in-page exclusion collection. It reports whether collection is enabled, the effective selector config, request-batching split summaries, the last collection surface (dom, programmatic, framework, or react), collected/skipped counts, last sent histories.exclude entries, diagnostics-only origin/source attribution, and recent auto-exclusion warnings.

  • diagnostics.autoExclusions.enabled and effectiveConfig: whether automatic exclusion collection is active and which effective selector/source config was used most recently.

  • diagnostics.autoExclusions.lastBatchingAt: timestamp of the last request-batching diagnostics update.

  • diagnostics.autoExclusions.lastBatchingSurface: surface that produced the last batching diagnostics update (dom, programmatic, framework, or react).

  • diagnostics.autoExclusions.lastBatchingSplitByExclusions: true when the last React or framework/queued recommendation batch had to be split into multiple outgoing requests because placements resolved to different histories.exclude sets. Use this together with lastBatchingPlacementCount, lastBatchingOutgoingRequestCount, and lastBatchingExclusionSignatureCount to verify whether in-page deduplication changed request coalescing.

  • diagnostics.autoExclusions.lastBatchingPlacementCount, lastBatchingOutgoingRequestCount, and lastBatchingExclusionSignatureCount: batching counters for the last grouped recommendation pass.

  • diagnostics.autoExclusions.lastCollectionAt: timestamp of the last exclusion collection pass.

  • diagnostics.autoExclusions.lastCollectionSurface: surface that performed the last exclusion collection pass (dom, programmatic, framework, or react).

  • diagnostics.autoExclusions.lastCollectedCount: number of exclusions collected in the last collection pass before request sending.

  • diagnostics.autoExclusions.lastSkippedCurrentPlacementCount: number of collected candidates skipped because they belonged to the current recommendation placement and includePlacement was not enabled.

  • diagnostics.autoExclusions.lastSkippedInvisibleCount: number of selector candidates skipped because the source required visible elements and the candidate was not visible.

  • diagnostics.autoExclusions.lastMissingIdCount: number of selector candidates skipped because no usable item id could be extracted.

  • diagnostics.autoExclusions.lastSourceCounts: count of last collected exclusions by source type before request sending.

  • diagnostics.autoExclusions.lastSentCount: number of exclusions sent on the most recent recommendation request.

  • diagnostics.autoExclusions.lastSentOriginCounts: count of last sent exclusions by primary diagnostics origin. Supported origins are manual-api, request-option, selector, and unknown.

  • diagnostics.autoExclusions.lastSentExclusions[].origin, source, and origins: diagnostics-only attribution for a sent exclusion. For example, manual-api means addExclusions(…​), request-option means request-level exclusions, and selector means DOM selector collection. These fields are not sent to the recommendation API.

  • diagnostics.autoExclusions.warnings: recent auto-exclusion-specific dev/support warnings such as invalid selectors, unusable regexes, or raw histories override warnings.

To support deviceIdContinuity across reloads and tabs, the browser SDK stores a small diagnostics snapshot in first-party localStorage under __froomle_device_diagnostics_v1.

This snapshot is for support diagnostics only:

  • it is not sent to the backend,

  • it is not used to choose recommendations,

  • it is not used to decide whether events are sent.

Compatibility note:

  • window.FroomleFrontendSdkReady remains available and aliases window.FroomleFrontendSdkRuntime.init.ready.

  • window.FroomleFrontendSdkDiagnostics remains available as a convenience alias to window.FroomleFrontendSdkRuntime.integration.

  • For new browser-side diagnostics and support tooling, prefer window.FroomleFrontendSdkRuntime and treat diagnostics as observational support state, not as a new control surface.

Script-tag/global DOM attribute contract:

  • Treat window.FroomleFrontendSdkRuntime as the canonical live runtime state.

  • In script-tag/global mode, the live SDK <script> element mirrors only:

    • data-froomle-consent after setFroomleConsent(…​) / window.FroomleFrontendSdk.setConsent(…​) / event.detail.sdk.setConsent(…​)

    • data-froomle-user-id after setUserId(…​)

    • data-froomle-channel after setChannel(…​)

  • This live mirroring applies both in froomle:before-init and later same-page setter calls.

  • Other data-froomle-* attributes remain boot inputs unless explicitly documented otherwise.

  • In particular, data-froomle-device-id stays an explicit customer override input and is not backfilled from SDK-managed browser identity.

Settings Matrix (Script-Tag Mode)

Use this matrix as the canonical checklist for what to configure where.

This matrix is only for script-tag/global integrations (froomle.global.js). React/module-import integrations do not use these hooks.

Setting Script tag attribute? Must be set before first request? Recommended place and usage

environment (setEnvironment)

Yes: data-froomle-env

Yes (required)

Put on SDK <script> tag.

page_visit (setPageVisit)

Yes: data-froomle-page-visit

Yes (required)

Put on SDK <script> tag.

request_domain (setRequestDomain)

Yes: data-froomle-request-domain

Yes (if custom domain is needed)

Use script tag for fixed value; use froomle:before-init if runtime-derived.

safe_request (setSafeRequest)

Yes: data-froomle-safe-request

Yes (if changed from default)

Use script tag for fixed value; use froomle:before-init for runtime decisions.

recommendation bearer (setRecommendationsBearer)

No

Yes (if the first recommendation request requires auth)

Set in froomle:before-init when your backend already rendered a short-lived token into the page.

recommendation bearer provider (setRecommendationsBearerProvider)

No

Yes (if the first recommendation request requires auth)

Set in froomle:before-init when the browser should fetch or refresh a token from your backend.

context_item / context_item_type

Yes: data-froomle-context-item, data-froomle-context-item-type

Yes (on detail pages)

Use script tag for fixed detail pages, or set in froomle:before-init.

user_id (setUserId)

Yes: data-froomle-user-id

Yes (if first request must be identified)

Fixed value: script tag. Runtime value (cookie/session): froomle:before-init.

device_id (setDeviceId)

Yes: data-froomle-device-id

Yes (if first request must carry device identity)

Fixed value: script tag. Runtime value: froomle:before-init.

channel (setChannel)

Yes: data-froomle-channel

Yes (if channel affects first request)

Fixed channel: script tag. Dynamic channel (viewport/device): froomle:before-init.

consent (setFroomleConsent / sdk.setConsent)

No

Yes (if first request must respect consent level)

Set in froomle:before-init. Script-tag alias: setFroomleConsent(level). Namespaced equivalent: event.detail.sdk.setConsent(level).

custom variables (setCustomVariable)

No

Yes, if variable must affect first request

First-request variables: froomle:before-init. Late/non-critical variables: froomle:init or window.FroomleFrontendSdkReady.

histories (addHistories)

No

Yes, if histories must influence first request

Set in froomle:before-init.

Post-init orchestration (non-settings)

N/A

No

Use froomle:init and/or window.FroomleFrontendSdkReady for UI wiring, telemetry, and late modules.

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
    sdk.setCustomVariable('section', 'homepage')
  }, { once: true })

  window.addEventListener('froomle:init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    // Post-init code
  })
</script>

defer (default)

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
    sdk.setCustomVariable('section', 'homepage')
  }, { once: true })
</script>
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>
<script>
  window.FroomleFrontendSdkReady.then(function () {
    // Post-init code
  })
</script>

blocking (legacy compatibility)

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
  }, { once: true })
</script>
<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>
<script>
  window.FroomleFrontendSdkReady.then(function () {
    // Post-init code
  })
</script>

async (supported with readiness gating)

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
  }, { once: true })
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>
<script>
  window.FroomleFrontendSdkReady.then(function () {
    // Post-init code
  })
</script>

Event-driven

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
    sdk.setCustomVariable('init_source', 'before-init')
  }, { once: true })

  window.addEventListener('froomle:init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    // Post-init code
  })
</script>
<script
  async
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>
<script>
  window.FroomleFrontendSdkReady.then(function () {
    // Late-safe fallback
  })
</script>

froomle:before-init and froomle:init event payload:

Field Meaning

event.detail.sdk

SDK API object available at init time.

event.detail.mode

Init trigger mode: auto or manual.

event.detail.version

SDK version string when available.

mode values:

  • auto: the SDK started itself during script evaluation (standard behavior in script-tag usage).

  • manual: the SDK was started via init().

In normal script-tag integrations, you will usually see mode=auto.

Module Setup (JS/TS/React/JSP Bundles)

npm install @froomle/frontend-sdk

Initialize shared SDK state in code:

import { setEnvironment, setPageVisit, setConsent } from '@froomle/frontend-sdk'

setEnvironment('your_env')
setPageVisit('home')
setConsent(2)

Module/import setup has two valid bootstrap shapes:

  • React and purely programmatic JS/TS integrations do not call init().

  • If your module/import integration also uses declarative data-froomle-* placeholders, call init() explicitly after the shared SDK setters to start DOM placeholder mode.

Use the consent setter name that matches how the SDK is loaded:

Runtime mode Consent call

Script-tag/global (froomle.global.js)

setFroomleConsent(level)
or window.FroomleFrontendSdk.setConsent(level)

Module/import (npm install @froomle/frontend-sdk)

setConsent(level) from import { setConsent } …​

Local Development

For static HTML files, avoid file:// and run a local server:

python3 -m http.server 8080

For module projects, run your framework/bundler dev command (for example npm run dev or npm run serve).

Core Configuration

This section defines setting semantics shared across runtimes. For setter scope (shared page/app runtime state vs per-block or per-request configuration), use Runtime Scope Guide. For script-tag timing and hook placement (what must be set before the first request), use Script Loading and Initialization Modes and Supported Inputs by Location. React follows module/import setup and does not use script-tag init hooks.

Setter Reference and Defaults

Setting Default Script-tag attribute Typical use

setEnvironment(env)

null

data-froomle-env

Always set to your environment.

setPageVisit(pageType)

null

data-froomle-page-visit

Always set for recommendation requests.

setConsent(level)

0

(none)

In module/import setups, use setConsent(level). In script-tag/global setups, use setFroomleConsent(level).

setDeviceId(id)

no-consent

data-froomle-device-id

Optional override when you manage device IDs yourself.

setUserId(id)

null

data-froomle-user-id

Use for stable logged-in identity.

setRequestDomain(host)

europe-west1.froomle.com

data-froomle-request-domain

Override domain (custom API domain/proxy/local setup).

setRecommendationsBearer(token)

(none)

(none)

Add Authorization: Bearer …​ to recommendation requests only. Use when your backend already injected a short-lived token.

clearRecommendationsBearer()

(none)

(none)

Clear the cached recommendation bearer token, for example on logout or tenant switch.

setRecommendationsBearerProvider(provider)

(none)

(none)

Async provider for recommendation requests only. The SDK waits for it before the first request and retries once after a 401 with forceRefresh: true.

clearRecommendationsBearerProvider()

(none)

(none)

Remove the active recommendation token provider, for example on logout or tenant switch.

setChannel(channel)

www-desktop

data-froomle-channel

Force channel or set responsive channel in runtime code.

setSafeRequest(bool)

true

data-froomle-safe-request

Set to false for non-HTTPS local/dev domains.

setContextItem(itemId)

null

data-froomle-context-item

Detail pages.

setContextItemType(type)

article

data-froomle-context-item-type

Detail pages with non-default type.

setCustomVariable(key, value)

(none)

(none)

Request-root variables and custom business data.

setBenchmark(config)

(none, defaults to froomle-managed semantics when benchmark config is present)

data-froomle-benchmark-*

Define default benchmark behavior for later requests and placements.

getBenchmark()

(none)

(none)

Read the current default benchmark config from SDK runtime state.

clearBenchmark()

(none)

(none)

Remove the current default benchmark config from SDK runtime state.

addHistories(histories, defaultItemType?)

empty

(none)

Add history IDs or typed history entries ({ id, item_type } or { id, itemType }).

addExclusions(exclusions, defaultItemType?)

empty

(none)

Persistently exclude item IDs from later recommendation responses by sending histories.exclude.

clearExclusions()

empty

(none)

Clear persistent SDK-managed exclusions.

setAutoExclusions(config | false)

disabled

data-froomle-auto-exclude="page"

Enable or configure per-request page item exclusion collection.

getAutoExclusions()

disabled

(none)

Read the current auto-exclusion config.

clearAutoExclusions()

disabled

(none)

Disable and clear the current auto-exclusion config.

Supported Inputs by Location

Location Runtime scope Inputs What This Is For

SDK script tag

Script-tag/global only

  • data-froomle-env

  • data-froomle-page-visit

  • data-froomle-context-item

  • data-froomle-context-item-type

  • data-froomle-request-domain

  • data-froomle-device-id

  • data-froomle-user-id

  • data-froomle-safe-request

  • data-froomle-channel

  • data-froomle-benchmark-mode

  • data-froomle-benchmark-current-group

  • data-froomle-benchmark-control-group

  • data-froomle-benchmark-treatment-group

  • data-froomle-auto-exclude

  • data-froomle-auto-exclude-selector

  • data-froomle-auto-exclude-id-attribute

  • data-froomle-auto-exclude-id-regex

  • data-froomle-auto-exclude-item-type

  • data-froomle-auto-exclude-include-placement

Bootstraps SDK from HTML without imports. Only this whitelist is read from the script tag. Recommendation auth is configured via JS setters, not script-tag attributes.

Recommendation markup

All runtimes

  • data-froomle-reco

  • data-froomle-param-*

  • data-froomle-variable-*

  • data-froomle-reco-filter-*

  • data-froomle-smart-sort

  • data-froomle-smart-sort-item

  • data-froomle-rank

  • optional placement benchmark overrides: data-froomle-benchmark-mode, data-froomle-benchmark-current-group, data-froomle-benchmark-control-group, data-froomle-benchmark-treatment-group

  • ordering attributes: data-froomle-order-*, data-froomle-ordervalue

Declarative recommendation rendering in the DOM. Use this to map item fields into existing HTML placeholders.

JS/TS module API

Module/import runtimes

  • setEnvironment

  • setPageVisit

  • setConsent

  • setDeviceId

  • setUserId

  • setRequestDomain

  • setSafeRequest

  • setChannel

  • setRecommendationsBearer

  • clearRecommendationsBearer

  • setRecommendationsBearerProvider

  • clearRecommendationsBearerProvider

  • setContextItem

  • setContextItemType

  • setCustomVariable

  • setBenchmark

  • getBenchmark

  • clearBenchmark

  • addHistories

  • addExclusions

  • clearExclusions

  • setAutoExclusions

  • getAutoExclusions

  • clearAutoExclusions

  • init

  • getRecommendations

  • smartSort

  • proxyReco

  • fulfillRecommendations

  • sendItemInteraction

  • sendUserInteraction

  • sendAddToCart

  • sendRemoveFromCart

  • sendPurchase

  • sendEvent

Code-first integration. Use this for explicit runtime control, programmatic fetching, and manual event flows. If you also render declarative data-froomle-* placeholders from a module/import setup, call init() explicitly to start DOM placeholder mode.

React bindings

React only

  • FroomleSdkInit

  • useCreateReco

  • useSmartSort

  • FroomleReco

  • useReco

  • FroomleOrder

  • FroomleOrderItem

  • FroomleCustomItem

React component/hook layer on top of core SDK APIs. Use this when recommendations are composed directly in React components.

Programmatic Method Quick Reference

Method Purpose

getRecommendations(lists)

Fetch recommendation lists directly in code.

smartSort(request, options?)

Rerank caller-provided candidate items by sending them as list_content.

proxyReco(request)

Queue a recommendation request and get a promise-like recommendation handle.

fulfillRecommendations()

Flush queued proxy requests in one batched API call.

sendEvent(actionItem, actionItemType, eventType?, extras?, options?)

Low-level fallback for explicit custom/manual tracking events.

setBenchmark(config)

Set the default benchmark config for later requests and placements.

getBenchmark()

Read the current default benchmark config.

clearBenchmark()

Remove the current default benchmark config.

addExclusions(exclusions, defaultItemType?)

Persistently add item IDs to request-local histories.exclude entries. String entries default to article; object entries may use item_type or itemType. These exclusions may be sent on anonymous recommendation requests; they do not add pageview history.

clearExclusions()

Clear persistent exclusions added through addExclusions(…​).

setAutoExclusions(config | false)

Enable or configure opt-in automatic exclusion collection from page markup such as data-froomle-id.

getAutoExclusions()

Read the current auto-exclusion config.

clearAutoExclusions()

Disable and clear auto-exclusion collection.

sendItemInteraction(actionItem, actionItemType, interactionType, extras?) sendItemInteraction(interactionType, extras?)

Send item_interaction for flows such as wishlists, likes, or favorites. The one-argument form reuses the current context_item / context_item_type.

sendUserInteraction(interactionType, extras?)

Send user_interaction for non-item flows such as review submits or chat starts.

sendAddToCart(actionItem, actionItemType, amount, extras?) sendAddToCart(amount, extras?)

Send add_to_cart with required amount and optional basket_content. The short form reuses the current context_item / context_item_type.

sendRemoveFromCart(actionItem, actionItemType, amount, extras?) sendRemoveFromCart(amount, extras?)

Send remove_from_cart with required amount and optional basket_content. The short form reuses the current context_item / context_item_type.

sendPurchase(actionItem, actionItemType, amount, purchasedPrice, extras?) sendPurchase(amount, purchasedPrice, extras?)

Send purchase with required amount and purchased_price. The short form reuses the current context_item / context_item_type.

setRecommendationsBearer(token)

Configure a static recommendation bearer token.

setRecommendationsBearerProvider(provider)

Configure an async recommendation token provider.

clearRecommendationsBearer()

Clear the cached recommendation bearer token.

clearRecommendationsBearerProvider()

Clear the configured recommendation token provider.

TypeScript Types and Interfaces

The SDK publishes declaration files via package exports:

  • @froomle/frontend-sdkdist/index.d.ts

  • @froomle/frontend-sdk/reactdist/react.d.ts

Typical TS imports:

import {
  addExclusions,
  addHistories,
  clearBenchmark,
  clearAutoExclusions,
  clearExclusions,
  clearRecommendationsBearer,
  clearRecommendationsBearerProvider,
  fulfillRecommendations,
  getAutoExclusions,
  getBenchmark,
  getRecommendations,
  proxyReco,
  sendAddToCart,
  sendEvent,
  sendItemInteraction,
  sendPurchase,
  sendRemoveFromCart,
  sendUserInteraction,
  setAutoExclusions,
  setBenchmark,
  setConsent,
  setRecommendationsBearer,
  setRecommendationsBearerProvider,
  setDeviceId,
  setEnvironment,
  setPageVisit
} from '@froomle/frontend-sdk'
import type { RecommendationItem, Recommendations } from '@froomle/frontend-sdk'

The package exports RecommendationItem and Recommendations as types. Some helper request/input shapes are currently inferred from function signatures instead of exported as named types. The following shapes mirror current SDK declarations:

type RecoRequestShape = {
  list: string
  filters: Array<{ key: string; value: string }>
  others?: Array<{ key: string; value: unknown }>
  exclusions?: ExclusionInput[]
  autoExclusions?: AutoExclusionsConfig | boolean
}

type ExclusionInput =
  | string
  | { id: string; item_type?: string; itemType?: string }

type ExcludeItemsConfig<TItem> = {
  items: readonly TItem[]
  getId: (item: TItem, index: number) => string | null | undefined
  getItemType?: (item: TItem, index: number) => string | null | undefined
  itemType?: string
}

type AutoExclusionsConfig = {
  enabled?: boolean
  scope?: 'page'
  includePlacement?: boolean
  selector?: string
  idAttribute?: string
  idRegex?: string
  idRegexGroup?: number | string
  itemType?: string
  itemTypeAttribute?: string
  visibleOnly?: boolean
  sources?: Array<{
    selector: string
    idAttribute?: string
    idRegex?: string
    idRegexGroup?: number | string
    itemType?: string
    itemTypeAttribute?: string
    visibleOnly?: boolean
  }>
}

type HistoryInputShape =
  | string
  | { id: string; item_type?: string | null; itemType?: string | null }

type SendEventExtrasShape = {
  [key: string]: string | number | object
}

type BasketContentItemShape = {
  id: string
  [key: string]: string | number | object | undefined
}

Current signature highlights from the published declarations:

setConsent(consent: number): void
setUserId(userId: string): void
setDeviceId(deviceId: string): void
setBenchmark(config: {
  mode?: "froomle-managed" | "customer-managed"
  currentGroup?: string | null
  groups?: { control?: string; treatment?: string }
} | null): void
getBenchmark(): {
  mode?: "froomle-managed" | "customer-managed"
  currentGroup?: string | null
  groups?: { control?: string; treatment?: string }
} | null
clearBenchmark(): void
setRecommendationsBearer(token: string | null | undefined): void
setRecommendationsBearerProvider(provider: (options: { forceRefresh: boolean }) => string | Promise<string>): void
setCustomVariable(key: string, value: unknown): void

getRecommendations(lists: Array<{
  limit: number
  list_name: string
  list_size: number
  [key: string]: unknown
}>, options?: {
  exclusions?: ExclusionInput[]
  autoExclusions?: AutoExclusionsConfig | boolean
  benchmark?: {
    mode?: "froomle-managed" | "customer-managed"
    currentGroup?: string | null
    groups?: { control?: string; treatment?: string }
  } | null
}): Promise<Recommendations>

sendEvent(
  actionItem: string | null | undefined,
  actionItemType: string | null | undefined,
  eventType?: string,
  extras?: { [key: string]: string | number | object | undefined },
  options?: { source?: Element | null }
): void

sendItemInteraction(
  actionItem: string,
  actionItemType: string,
  interactionType: string,
  extras?: { [key: string]: string | number | object | undefined }
): void
sendItemInteraction(
  interactionType: string,
  extras?: { [key: string]: string | number | object | undefined }
): void

sendUserInteraction(
  interactionType: string,
  extras?: { [key: string]: string | number | object | undefined }
): void

sendAddToCart(
  actionItem: string,
  actionItemType: string,
  amount: number,
  extras?: {
    basket_content?: BasketContentItemShape[]
    [key: string]: string | number | object | undefined
  }
): void
sendAddToCart(
  amount: number,
  extras?: {
    basket_content?: BasketContentItemShape[]
    [key: string]: string | number | object | undefined
  }
): void

sendRemoveFromCart(
  actionItem: string,
  actionItemType: string,
  amount: number,
  extras?: {
    basket_content?: BasketContentItemShape[]
    [key: string]: string | number | object | undefined
  }
): void
sendRemoveFromCart(
  amount: number,
  extras?: {
    basket_content?: BasketContentItemShape[]
    [key: string]: string | number | object | undefined
  }
): void

sendPurchase(
  actionItem: string,
  actionItemType: string,
  amount: number,
  purchasedPrice: number,
  extras?: {
    original_price?: number
    [key: string]: string | number | object | undefined
  }
): void
sendPurchase(
  amount: number,
  purchasedPrice: number,
  extras?: {
    original_price?: number
    [key: string]: string | number | object | undefined
  }
): void

useCreateReco(req: {
  list: string
  filters: Array<{ key: string; value: string }>
  exclusions?: ExclusionInput[]
  excludeItems?: ExcludeItemsConfig<unknown>
  autoExclusions?: AutoExclusionsConfig | boolean
  [key: string]: any
}): RecommendationItem & PromiseLike<RecommendationItem>

unknown is intentional for pass-through/custom payload values (for example setCustomVariable values and extra list keys in getRecommendations list entries). This keeps SDK typings strict without guessing backend-specific business fields.

Behavior note:

  • repeated filter keys merge into arrays

  • repeated setCustomVariable(…​) calls keep the last value

  • pass arrays explicitly for multi-value custom variables

Environment and Page Visit (Required)

Set both before requesting recommendations.

Script-tag setup:

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
></script>

Module setup:

import { setEnvironment, setPageVisit } from '@froomle/frontend-sdk'

setEnvironment('sample_env')
setPageVisit('home')

Identity: device_id and user_id

Identity usage model:

  • setUserId(…​): set when the user has a stable logged-in ID.

  • setDeviceId(…​): advanced override only; use it only when you manage device IDs yourself (or intentionally unify with your own device ID strategy).

  • In normal browser integrations, let the SDK manage device_id through froomle_device_id.

For consent-driven identity behavior (user_id, device_id, and no-consent flows), see User identity and consent.

user_id should be a real logged-in identifier or omitted. Never set user_id to "no-consent".

If you do not set device_id, the SDK manages it automatically via froomle_device_id and generates it when consent allows tracking. For logged-in users, provide user_id; device_id can be SDK-managed or explicitly set by your own device-ID strategy.

Browser SDK contract:

  • Consent levels 0 and 1 are anonymous states. Requests/events use device_id: "no-consent", omit user_id, and do not send histories.

  • Consent level 2 is the identified state. The SDK persists a browser-managed froomle_device_id and reuses it across consented reloads.

  • If consent drops from 2 to 0 or 1, the SDK returns to anonymous no-consent identity. The previously identified SDK-managed device_id must not remain active while consent stays below 2.

  • If consent later returns to 2 after an anonymous interval, the SDK creates a fresh identified device_id instead of silently reviving the previous one.

  • Reapplying the same consent level must not rotate device_id.

  • A real user_id may coexist with the SDK-managed device_id at consent level 2.

It should never happen that one device_id is associated with multiple user_id values in the same environment.

Script-tag equivalents:

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
  data-froomle-device-id="device-123"
></script>

or

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
  data-froomle-user-id="user-456"
></script>

The data-froomle-device-id script attribute is an explicit override example, not the recommended default for browser integrations.

Identity and Storage (Cookies and Diagnostics Storage)

In browser/script-tag usage, the SDK persists identity and consent in first-party cookies:

  • froomle_device_id

  • froomle_user_id

  • froomle_consent

For browser runtime diagnostics only, the SDK also stores a small first-party localStorage snapshot:

  • __froomle_device_diagnostics_v1

Behavior:

  • On load, the SDK reads existing cookie values and reuses them.

  • If identity is passed via script attributes or setters, the SDK updates the matching cookie.

  • Calling setFroomleConsent(…​) (or window.FroomleFrontendSdk.setConsent(…​)) updates froomle_consent.

  • In script-tag/global mode, the live SDK <script> element also mirrors data-froomle-consent, data-froomle-user-id, and data-froomle-channel after the corresponding setters run, including late same-page updates.

  • Other data-froomle-* attributes remain boot inputs. In particular, data-froomle-device-id is not rewritten from SDK-managed identity.

  • When consent becomes 2 and device_id is still no-consent, the SDK generates a UUID device id and stores it.

  • If consent later drops to 0 or 1, the active SDK identity returns to no-consent.

  • If consent later returns to 2 after an anonymous interval, the SDK creates a fresh identified device_id instead of reviving the previous one.

  • Reapplying the same consent level does not rotate device_id.

  • The localStorage diagnostics snapshot is used only to compare device-id continuity across reloads and tabs for support/debug purposes.

  • The localStorage diagnostics snapshot is not sent to the backend and does not affect recommendation or event-tracking behavior.

Cookie properties in the current SDK build:

  • first-party cookie

  • path=/

  • samesite=lax

  • long-lived max-age (20 years in current implementation)

Older integrations may reference _fr_id; the current SDK implementation uses froomle_device_id.

Use setConsent(level) in modules. In script-tag integrations, setFroomleConsent(level) is the browser-global alias.

setConsent(…​) is not a bare global function in plain HTML. In script-tag pages, use setFroomleConsent(…​) or window.FroomleFrontendSdk.setConsent(…​).

setConsent(2)
// or, in plain HTML/global usage:
setFroomleConsent(2)

If your site uses an external consent manager or CMP, treat the CMP as the source of truth and bridge its state into the SDK.

Recommended sequence:

  1. Register the CMP ready and consent-change hooks before the SDK boots.

  2. Capture event.detail.sdk in froomle:before-init.

  3. Read the current CMP consent state immediately.

  4. Subscribe to future CMP consent changes.

  5. Call sdk.setConsent(level) only from the CMP state, not from undocumented dataLayer payload assumptions.

Contract notes:

  • Let the SDK manage device_id in normal browser integrations. Do not call setDeviceId(…​) from the CMP bridge unless your backend contract intentionally owns device identity.

  • Consent levels 0 and 1 are anonymous states; consent level 2 is the identified state.

  • If the CMP moves from 2 to 0 or 1, the SDK returns to anonymous no-consent identity.

  • If the CMP later moves from 0 or 1 back to 2, the SDK creates a fresh identified device_id rather than reviving the previous one.

  • Reapplying the same consent level should not rotate device_id.

Generic script-tag shape:

<script>
  var froomleSdk = null
  var pendingConsent = false

  function cmpHasTrackingConsent() {
    // Replace with the supported API call from your CMP.
    return false
  }

  function applyFroomleConsent() {
    if (!cmpHasTrackingConsent()) return false

    if (!froomleSdk || typeof froomleSdk.setConsent !== "function") {
      pendingConsent = true
      return false
    }

    froomleSdk.setConsent(2)
    pendingConsent = false
    return true
  }

  function onCmpReady(callback) {
    // Replace with your CMP's documented ready hook.
  }

  function onCmpConsentChanged(callback) {
    // Replace with your CMP's documented consent-change hook.
  }

  onCmpReady(applyFroomleConsent)
  onCmpConsentChanged(applyFroomleConsent)

  window.addEventListener('froomle:before-init', function (event) {
    froomleSdk = event.detail && event.detail.sdk
    if (pendingConsent || cmpHasTrackingConsent()) {
      applyFroomleConsent()
    }
  }, { once: true })
</script>

Use the CMP’s official API and event hooks whenever they exist. Keep dataLayer parsing only as a fallback when the provider explicitly documents it for your integration.

Level Name Recommendation requests Tracking events

0

No Tracking

Anonymous request (device_id, user_group, version set to no-consent), no user_id, no pageview histories. Request-local histories.exclude may still be sent for response deduplication.

Disabled

1

Analytics Only

Same anonymous recommendation request contract as level 0. Tracking events are anonymous.

Enabled, but anonymous identity

2

Full

Identified request (device_id, and user_id when set), histories allowed

Enabled with identified identity

Practical behavior:

  • Consent 0: popular/non-personalized behavior, no tracked events.

  • Consent 1: event tracking enabled, personalization identity still anonymous.

  • Consent 2: full personalization and identified tracking when identity is set.

Consent level Behavior

0

Requests remain anonymous, user_id and pageview histories are excluded, and tracking events are not emitted. Request-local histories.exclude may still be sent for response deduplication.

1

Requests remain anonymous and omit pageview histories; tracking events are emitted with anonymous identity. Request-local histories.exclude may still be sent for response deduplication.

2

Requests may include full identity and histories; tracking events are emitted with identified identity.

Detail Context and Page Tracking

For detail pages, set context item (and optional type):

import { setContextItem, setContextItemType, setPageVisit } from '@froomle/frontend-sdk'

setPageVisit('article_detail')
setContextItem('sample_item_id_2')
setContextItemType('article')

Script-tag equivalent:

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="article_detail"
  data-froomle-context-item="sample_item_id_2"
  data-froomle-context-item-type="article"
></script>

Behavior notes:

  • context_item_type is optional; when omitted and context_item is set, SDK defaults it to article.

  • setContextItem(…​) and setContextItemType(…​) only define the detail-page context. They do not send detail_pageview by themselves.

  • The same context (context_item / context_item_type) is used for recommendation requests and detail tracking.

  • When context_item_type is omitted, detail_pageview.action_item_type also defaults to article.

  • Automatic detail_pageview requires the page-level SDK init pipeline to run on that page.

    • Script-tag/global integrations get this from the normal SDK bootstrap on the detail page.

    • React/framework integrations get this from mounting FroomleSdkInit on the detail page.

  • When context_item is present and the page-level init pipeline runs, SDK emits detail_pageview for the page and suppresses the generic page_visit event.

  • If you only need article tracking on a detail page, you can still initialize the SDK there without rendering any recommendation blocks.

Request Domain, Channel, and Transport

Use these when needed:

  • setRequestDomain('your.domain')

  • setSafeRequest(false) for non-HTTPS/local environments

  • setChannel('www-desktop' | 'www-mobile')

If you want to run SDK traffic on your own subdomain, follow Custom domain (CNAME) for DNS, SSL, and SDK setup steps.

Responsive channel example:

import { setChannel } from '@froomle/frontend-sdk'

const channel = window.matchMedia('(max-width: 767px)').matches
  ? 'www-mobile'
  : 'www-desktop'
setChannel(channel)

Recommendation Authentication

By default, browser SDK integrations do not need bearer auth for recommendation requests. Use the auth setters only when your recommendation API is configured to require a bearer token in the browser.

Recommendation auth only affects recommendation requests. Tracked events (page_visit, detail_pageview, impression, click_on_recommendation) remain unauthenticated in the current SDK.

Rules:

  • Keep client_id and client_secret on your backend.

  • Do not call /oauth/token from the browser.

  • Configure auth before the first recommendation request.

  • Use setRecommendationsBearer(token) when your backend already rendered a short-lived token into the page.

  • Use setRecommendationsBearerProvider(async ({ forceRefresh }) ⇒ token) when the browser should ask your backend for a token.

This section only covers how the frontend SDK receives and uses a bearer token. The actual OAuth token flow, client-credentials exchange, token refresh strategy, and backend responsibilities are documented in Authentication Flow.

When to use which pattern:

  • Static bearer: backend-rendered pages, SSR templates, or any flow where a valid token is already available synchronously.

  • Provider: frontend apps that need to fetch a token from a minimal backend endpoint and refresh it after a 401.

Static bearer in script-tag/global usage:

<script>
  window.__FROOMLE_RECO_BEARER__ = '{{ server_rendered_short_lived_token }}'

  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return

    sdk.setConsent(2)
    sdk.setRecommendationsBearer(window.__FROOMLE_RECO_BEARER__)
  }, { once: true })
</script>
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>

Static bearer in module and React usage:

import {
  setConsent,
  setEnvironment,
  setPageVisit,
  setRecommendationsBearer
} from '@froomle/frontend-sdk'

setEnvironment('your_env')
setPageVisit('home')
setConsent(2)
setRecommendationsBearer(window.__FROOMLE_RECO_BEARER__)

Provider behavior:

  • The SDK waits for the provider before the first recommendation request.

  • If the recommendation request returns 401, the SDK calls the provider once more with forceRefresh: true.

  • The SDK retries the recommendation request once with the refreshed token.

Provider callback in script-tag/global usage:

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return

    sdk.setConsent(2)
    sdk.setRecommendationsBearerProvider(async function (options) {
      var forceRefresh = !!(options && options.forceRefresh)
      var response = await fetch(
        '/your-backend/froomle-recommendations-token?refresh=' + (forceRefresh ? '1' : '0'),
        { credentials: 'include' }
      )

      if (!response.ok) {
        throw new Error('Token endpoint failed with status ' + response.status)
      }

      var body = await response.json()
      return body.token
    })
  }, { once: true })
</script>
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="your_env"
  data-froomle-page-visit="home"
></script>

Provider callback in module and React usage:

import {
  setConsent,
  setEnvironment,
  setPageVisit,
  setRecommendationsBearerProvider
} from '@froomle/frontend-sdk'

setEnvironment('your_env')
setPageVisit('home')
setConsent(2)
setRecommendationsBearerProvider(async ({ forceRefresh }) => {
  const response = await fetch(
    '/your-backend/froomle-recommendations-token?refresh=' + (forceRefresh ? '1' : '0'),
    { credentials: 'include' }
  )

  if (!response.ok) {
    throw new Error(`Token endpoint failed with status ${response.status}`)
  }

  const body = await response.json()
  return body.token
})

React uses the same auth setters from @froomle/frontend-sdk. The React package @froomle/frontend-sdk/react provides rendering bindings, not separate auth APIs.

Minimum Initialization

Minimal module-based setup (programmatic or React):

import { setEnvironment, setPageVisit, setConsent } from '@froomle/frontend-sdk'

setEnvironment('sample_env')
setPageVisit('home')
setConsent(2)

Minimal module + markup setup (explicit DOM mode):

import { init, setEnvironment, setPageVisit, setConsent } from '@froomle/frontend-sdk'

setEnvironment('sample_env')
setPageVisit('home')
setConsent(2)
init()

Minimal plain HTML setup:

<script>
  window.addEventListener('froomle:before-init', function (event) {
    var sdk = event.detail && event.detail.sdk
    if (!sdk) return
    sdk.setConsent(2)
  }, { once: true })
</script>
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
></script>

Recommendations in Markup

Recommendation Placeholder

data-froomle-reco marks a recommendation template. Each placeholder card usually maps fields with data-froomle-param-*.

How binding works:

  1. SDK requests items for each list.

  2. Each placeholder marked with data-froomle-reco receives one returned item.

  3. data-froomle-param-* attributes map item fields into DOM attributes/text.

  4. Filled nodes receive internal metadata attributes used for tracking.

<article class="reco-card" data-froomle-reco="recommended_for_you">
  <a data-froomle-param-href="uri" href="#">
    <img data-froomle-param-src="images[0]" data-froomle-param-alt="title" alt="" />
    <h3 data-froomle-param-inner="title">Loading recommendation...</h3>
  </a>
</article>

Use multiple placeholder blocks to control count (for example, 12 placeholders ⇒ up to 12 returned items).

This is a slot-based model: one placeholder represents one rendered recommendation item. When multiple placeholders target the same list with compatible configuration, the SDK batches them into a backend list request automatically.

Dynamic Injection and Refill

The SDK observes DOM mutations after initial load when declarative DOM mode is active.

This applies to:

  • script-tag/global usage (froomle.global.js)

  • module/import placeholder usage after explicit init()

It does not apply to React bindings by default. Raw declarative data-froomle-* blocks injected onto a React page stay idle unless you explicitly start DOM placeholder mode yourself.

This means:

  • New nodes injected with data-froomle-reco can be auto-filled after page load.

  • Existing nodes can be refilled when you clear metadata and toggle data-froomle-reco.

Refill rule in current runtime:

  • A target is eligible when both data-froomle-request-id and data-froomle-id are missing or empty.

  • To force refill, clear those attributes, then remove/re-set data-froomle-reco.

Recommendation Parameters: data-froomle-param-*

Common mappings:

  • data-froomle-param-href="uri"

  • data-froomle-param-src="images[0]"

  • data-froomle-param-alt="title"

  • data-froomle-param-inner="title"

Nested fields are supported, for example:

<span data-froomle-param-inner="item_attributes.fq_item_id"></span>

Expression mode is also supported by prefixing with =:

  • data-froomle-param-src="=photoUrl | replace('X,','NWB,') | replace('X.','NWB.')"

  • data-froomle-param-href="=uri | append('?from=reco')"

  • data-froomle-param-inner="=item_attributes.category_label | defaultField(item_attributes.main_tag) | default('No category')"

  • data-froomle-param-href="=item_attributes.category_url | defaultField(item_attributes.main_tag_link) | default('#')"

For composed values (for example srcset), use template placeholders:

  • data-froomle-param-srcset="\${photoUrl | replace('X,','NWB,')}, \${photoUrl | replace('X,','NWBR,')} 2x"

Scope:

  • This transform syntax is for declarative HTML mapping (data-froomle-param-*).

  • It is not executed inside framework prop systems (React/Vue directives).

  • In React, apply transforms in your component code and render final values in JSX.

Supported transforms:

  • replace(from, to)

  • prepend(value)

  • append(value)

  • default(value) (used when current value is empty)

  • defaultField(other.path) (used when current value is empty and you want to fall back to another recommendation field, for example during schema migrations or field renames)

  • trim

  • lower

  • upper

  • urlencode

What is supported:

  • Field access (including nested paths)

  • Transform chains with the supported transform set

  • Template interpolation with \${…​} for composed attributes (srcset, combined labels, etc.)

  • Rename-migration fallback patterns such as category_label → main_tag while old and new fields coexist

What is not supported:

  • Running custom code from attribute strings

  • if/else, loops, or arbitrary scripting in markup

  • Arbitrary transform arguments that execute as nested expressions

  • Regex/backreference syntax in transforms

Filters and Variables

Use list-level filters and variables in markup:

  • data-froomle-reco-filter-*: list filters (sent as arrays by SDK for HTML usage)

  • data-froomle-variable-*: list variables

<article
  data-froomle-reco="sample_list_name"
  data-froomle-reco-filter-race_types="MotoGP"
  data-froomle-reco-filter-article_type="News"
  data-froomle-variable-slot="1"
>
  <h3 data-froomle-param-inner="title"></h3>
</article>

How HTML values are converted:

  • data-froomle-reco-filter-* a single value such as MotoGP becomes ["MotoGP"]

  • data-froomle-reco-filter-* a semicolon-separated value such as Monde;Sports becomes ["Monde", "Sports"]

  • data-froomle-reco-filter-* a JSON array string such as ["Monde","Sports"] becomes ["Monde", "Sports"]

  • data-froomle-variable-* a single value such as homepage stays scalar

  • data-froomle-variable-* a semicolon-separated value becomes an array

  • data-froomle-variable-* a JSON array string becomes an array

Filter-key merge contract:

  • For JS/TS/React filter tuples, repeated filter keys merge into arrays in order.

  • Example: [{ key: "categories", value: "Monde" }, { key: "categories", value: "Sports" }] becomes "categories": ["Monde", "Sports"].

  • In plain HTML, duplicate attribute names are not a supported way to express that. Use one attribute whose value is already multi-valued, for example Monde;Sports or ["Monde","Sports"].

Custom variable contract:

  • Custom variables do not auto-merge on repeated setter calls.

  • setCustomVariable('campaign_id', 'spring') followed by setCustomVariable('campaign_id', 'summer') sends "campaign_id": "summer".

  • If a custom variable is intentionally multi-valued, pass the array explicitly in one value, for example setCustomVariable('access_types', ['FREE', 'PAID']).

Example request shapes:

{
  "lists": [
    {
      "list_name": "recommended_for_you",
      "categories": ["Monde"],
      "slot": "homepage"
    },
    {
      "list_name": "recommended_for_you",
      "categories": ["Monde", "Sports"],
      "access_types": ["FREE", "PAID"]
    }
  ]
}

List merging behavior:

  • Blocks with same list_name, same filters, and same list variables can be merged into one request.

  • Add a differentiator such as data-froomle-variable-slot to keep blocks separate.

Request-root variables must be set in JS/TS, not via markup:

import { setCustomVariable } from '@froomle/frontend-sdk'

setCustomVariable('section', '12345')

Type note: setCustomVariable is typed as setCustomVariable(key: string, value: unknown). Use application-side narrowing when you read values back in your own code.

Item IDs and Histories

Tag content items with IDs when possible:

<article class="news-card" data-froomle-id="sample_item_id_1">
  ...
</article>

This improves analytics/event attribution and helps history handling.

Contract note:

  • data-froomle-id is generic item identity and still contributes to histories.

  • Automatic recommendation impression / click_on_recommendation tracking requires resolved recommendation metadata as well, in particular data-froomle-request-id. Raw data-froomle-id authored in your own markup is not enough to activate recommendation auto-tracking by itself.

Inject histories (simple IDs):

import { addHistories } from '@froomle/frontend-sdk'

addHistories([
  'sample_item_id_1',
  'sample_item_id_2'
])

addHistories([…​]) generates histories.pageviews. For string IDs, item_type defaults to article.

Inject typed histories when explicit item_type is needed:

import { addHistories } from '@froomle/frontend-sdk'

addHistories([
  { id: 'sample_item_id_1', item_type: 'article' },
  { id: 'sample_item_id_2', item_type: 'video' },
  { id: 'sample_item_id_3', itemType: 'podcast' } // `itemType` alias is supported
], 'article') // fallback item_type for entries without explicit type

Behavior note:

  • addHistories(…​) appends entries to histories.pageviews in outgoing recommendation requests.

  • For string entries, SDK uses defaultItemType (or article when omitted).

  • For object entries, SDK uses item_type or itemType; if missing, it falls back to defaultItemType (or article).

  • setCustomVariable('histories', …​) writes the raw histories field directly only when consent allows histories. Under consent levels 0 and 1, raw custom histories is suppressed; generated/request-local histories.exclude may still be sent.

  • This is separate from context_item_type (used for detail context and detail_pageview), which defaults to article when omitted.

In-page Exclusions for Deduplication

Use exclusions when a recommendation list should avoid items that are already visible elsewhere on the same page. The SDK sends these as histories.exclude entries with reconsumable: false.

This is different from histories.pageviews:

  • histories.pageviews means the user has seen or consumed an item.

  • histories.exclude means the backend should avoid returning that item for this recommendation response.

  • Consent levels 0 and 1 suppress histories.pageviews but still allow request-local histories.exclude entries with reconsumable: false.

Recommended Production Order

Recommended production order:

Source Use when Notes

Stable data-froomle-id

You control the rendered page markup.

Preferred setup. Add data-froomle-id and, when known, data-froomle-item-type to every non-Froomle item that should be excluded from the recommendation response.

Configured CSS selector / attribute

The CMS or framework already renders stable item IDs, but under another attribute name.

Configure the selector and ID attribute explicitly. This is production-safe when the attribute is stable.

Regex extraction

The only stable ID is embedded in another value, for example an article URL.

Configure idRegex to extract the backend item ID from the selected attribute. Prefer a narrow selector and a tested regex.

Manual request exclusions

Your integration already has the visible item IDs in JavaScript.

Pass exclusions directly on the request. This avoids DOM scanning and is usually the clearest programmatic integration.

React excludeItems

Your React/CMS integration already has the visible item objects in memory.

Prefer this over DOM scanning for React pages. Pass the item array together with getId(…​) and itemType / getItemType(…​); the SDK maps it to request-level exclusions.

DOM / Script-tag Mode

In DOM/script-tag mode, mark items that are already visible on the page and opt in on the SDK script tag:

<article data-froomle-id="already-visible-article" data-froomle-item-type="article">
  ...
</article>

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
  data-froomle-auto-exclude="page"
></script>

Custom selector setup from script attributes:

<article data-article-url="/news/articles/article-123?utm=frontpage">
  ...
</article>

<script
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
  data-froomle-auto-exclude="page"
  data-froomle-auto-exclude-selector="[data-article-url]"
  data-froomle-auto-exclude-id-attribute="data-article-url"
  data-froomle-auto-exclude-id-regex="articles/([^/?#]+)"
  data-froomle-auto-exclude-item-type="article"
></script>

In DOM/script-tag mode, declarative data-froomle-reco placements still work normally. The exclusion scan runs before sending the recommendation request. By default, the SDK skips the current recommendation placement itself so placeholder metadata does not exclude the item it is about to request.

Module / JavaScript Setup

Module integrations can set a default auto-exclusion config once:

import { setAutoExclusions } from '@froomle/frontend-sdk'

setAutoExclusions(true) // default source: [data-froomle-id]

Or configure one or more custom sources:

setAutoExclusions({
  sources: [
    {
      selector: '[data-article-id]',
      idAttribute: 'data-article-id',
      itemType: 'article'
    }
  ]
})

The default config applies to later getRecommendations(…​), proxyReco(…​), and compatible framework requests. Passing autoExclusions on an individual request overrides the default for that request.

Manual and Programmatic Exclusions

Manual persistent exclusions apply to later recommendation requests until cleared:

import { addExclusions, clearExclusions } from '@froomle/frontend-sdk'

addExclusions([
  'sample_item_id_1',
  { id: 'sample_item_id_2', item_type: 'video' },
  { id: 'sample_item_id_3', itemType: 'podcast' }
])

// Later, when the exclusion scope no longer applies:
clearExclusions()

Request-level exclusions apply only to one recommendation request:

import { getRecommendations } from '@froomle/frontend-sdk'

await getRecommendations([
  { list_name: 'recommended_for_you', limit: 4, list_size: 4 }
], {
  exclusions: [
    { id: 'already-visible-article', item_type: 'article' }
  ]
})

Programmatic auto-exclusions scan the current DOM at request time:

import { getRecommendations } from '@froomle/frontend-sdk'

await getRecommendations([
  { list_name: 'recommended_for_you', limit: 4, list_size: 4 }
], {
  autoExclusions: {
    enabled: true,
    sources: [
      {
        selector: '[data-article-id]',
        idAttribute: 'data-article-id',
        itemType: 'article'
      }
    ]
  }
})

When using getRecommendations(…​) directly, the SDK shapes the recommendation request but does not own your rendering. If you also want automatic impression and click_on_recommendation tracking, render recommendation metadata into the DOM as described in Advanced Hybrid Pattern, or send the events manually.

React Mode

React hooks support exclusions, excludeItems, and autoExclusions request options. Render returned items through FroomleReco so automatic recommendation-item tracking still works.

Slot-based useCreateReco(…​):

const reco = useCreateReco({
  list: 'recommended_for_you',
  filters: [],
  autoExclusions: true
})

return (
  <FroomleReco reco={reco}>
    <ArticleCard />
  </FroomleReco>
)

List-based useRecoList(…​):

const { items } = useRecoList({
  list: 'recommended_for_you',
  limit: 4,
  list_size: 4,
  filters: [],
  autoExclusions: true
})

return items.map((item) => (
  <FroomleReco reco={item} key={item.Id}>
    <ArticleCard item={item} />
  </FroomleReco>
))

For React/CMS setups where the page already has visible article objects in memory, prefer excludeItems over DOM scanning. This is useful for ArcXP-style pages where the CMS composes the page from article objects and the recommendation lane should avoid articles already rendered above or elsewhere on the page:

const { items } = useRecoList({
  list: 'recommended_for_you',
  limit: cmsListSize,
  list_size: cmsListSize,
  filters: [],
  excludeItems: {
    items: articlesAlreadyRenderedAbove,
    getId: (article) => article._id,
    itemType: 'article'
  }
})

articlesAlreadyRenderedAbove is not a Froomle-specific object shape. It is the caller-owned array of CMS/ArcXP items that are already rendered, or are known to be rendered, on the page. The SDK does not send those full objects to Froomle. It only calls getId(…​) and getItemType(…​) / itemType to build request-local exclusion entries.

If the CMS objects expose the ArcXP id through _id, use:

excludeItems: {
  items: articlesAlreadyRenderedAbove,
  getId: (article) => article._id,
  itemType: 'article'
}

If only IDs are available, pass the ID array directly:

const alreadyRenderedArticleIds = [
  'JM43X5OXWJDSNDT2CGGDHWDUTY',
  'HTRDQNGS2ZANBMJUSB7E7KELDU'
]

const { items } = useRecoList({
  list: 'recommended_for_you',
  limit: cmsListSize,
  list_size: cmsListSize,
  excludeItems: {
    items: alreadyRenderedArticleIds,
    getId: (id) => id,
    itemType: 'article'
  }
})

This maps to histories.exclude entries like:

{
  "id": "JM43X5OXWJDSNDT2CGGDHWDUTY",
  "item_type": "article",
  "reconsumable": false
}

When you already have the exact exclusion IDs and do not need an item adapter, passing exclusions directly is also valid:

const { items } = useRecoList({
  list: 'recommended_for_you',
  limit: cmsListSize,
  list_size: cmsListSize,
  exclusions: alreadyRenderedArticleIds.map((id) => ({
    id,
    item_type: 'article'
  }))
})

excludeItems is only an adapter. The SDK maps it to request-level histories.exclude entries before sending the recommendation request. Use the items array to control scope: pass only items above the lane, all items in the surrounding matrix, or all known page items depending on the deduplication rule you want. In TypeScript, the item type is inferred from excludeItems.items, so getId(…​) and getItemType(…​) can read the CMS object shape directly.

Entry-based useRecoEntry(…​) for one request slot:

const entry = useRecoEntry({
  list: 'recommended_for_you',
  excludeItems: {
    items: articlesAlreadyRenderedAbove,
    getId: (article) => article._id,
    itemType: 'article'
  }
})

return entry ? (
  <FroomleReco entry={entry}>
    <ArticleCard item={entry.item} />
  </FroomleReco>
) : null

useReco(…​) reads the current FroomleReco context and does not send a recommendation request by itself. Configure exclusions on the request-producing hook: useCreateReco(…​), useRecoList(…​), or useRecoEntry(…​). For full raw-control benchmark examples with useRecoEntry(…​), see React Integrations.

Mode-by-mode Summary

Mode Enable with Where excluded IDs come from Tracking implication

DOM/script-tag

data-froomle-auto-exclude="page" and optional selector attributes on the SDK script tag.

Page markup, by default [data-froomle-id].

SDK-owned DOM rendering and tracking continue to work for data-froomle-reco placements.

Programmatic JS

getRecommendations(…​, { exclusions }) or getRecommendations(…​, { autoExclusions }).

Caller-provided IDs, or DOM scan at request time.

Recommendation rendering is customer-owned. Automatic events require SDK metadata in the DOM or manual event calls.

Framework queued requests

proxyReco(…​) request options or global setAutoExclusions(…​).

Request-level IDs, page markup, or global auto-exclusion config.

Compatible placements batch together; placements with different effective exclusion sets split into separate requests.

React useCreateReco(…​)

Hook option exclusions, excludeItems, autoExclusions, or global setAutoExclusions(…​).

Request-level IDs, caller-provided item arrays through excludeItems, or page markup scanned when the hook request is fulfilled.

Render with FroomleReco reco={reco} for automatic impression and click tracking.

React useRecoList(…​)

Hook option exclusions, excludeItems, autoExclusions, or global setAutoExclusions(…​).

Request-level IDs, caller-provided item arrays through excludeItems, or page markup scanned for the list request.

Render each item with FroomleReco reco={item} for automatic impression and click tracking.

React useRecoEntry(…​)

Hook option exclusions, excludeItems, autoExclusions, or global setAutoExclusions(…​).

Request-level IDs, caller-provided item arrays through excludeItems, or page markup scanned for the entry request.

Render with FroomleReco entry={entry} for automatic tracking and benchmark attribution.

Behavior Notes

  • Auto-exclusions are disabled by default.

  • Request-level autoExclusions overrides the global config from setAutoExclusions(…​) for that request.

  • Automatic collection is only reliable when the page exposes stable item IDs. Prefer data-froomle-id and data-froomle-item-type.

  • data-froomle-action-item is event metadata. It is not used for exclusions unless you explicitly configure it as a selector source.

  • By default, the SDK excludes explicit page items but not the current recommendation placement itself.

  • Exclusions are merged and deduplicated by item_type + id.

  • Queued placements with identical effective exclusion sets can still share one outgoing recommendation request.

  • Queued placements with different effective exclusion sets are split into separate outgoing recommendation requests, because histories.exclude applies to the full backend request.

  • Exclusions are request-local response filters. They may be sent under consent levels 0 and 1; behavioral pageview histories and raw custom histories are still suppressed.

  • The diagnostics object exposes diagnostics.autoExclusions so support can inspect what was collected, what was sent, whether exclusions caused request batching to split, and whether sent exclusions came from addExclusions(…​), request-level exclusions, or selector-based auto-collection.

Ordering Sections

Use ordering when you want to reorder existing blocks from recommendation signals.

Ordering Container

<div data-froomle-order-categories="recommended_for_you">
  ...
</div>

categories is the field to order by. recommended_for_you is the list used for ordering.

Elements To Order

Children that participate in ordering need data-froomle-ordervalue:

<div data-froomle-order-categories="recommended_for_you">
  <section data-froomle-ordervalue="Sports">Sports block</section>
  <section data-froomle-ordervalue="Politique">Politics block</section>
  <section data-froomle-ordervalue="Monde">World block</section>
</div>

Programmatic API (JS/TS)

Use programmatic mode when markup placeholders are not enough. Typical cases:

  • You want to fetch recommendations before rendering UI.

  • You want explicit batching control across multiple requests.

  • You render custom UI and must send events manually.

No data-froomle-* placeholders are required in this mode.

For benchmark-specific request and rendering patterns, see Benchmarking and A/B Testing with the SDK.

For setter scope, see Runtime Scope Guide. Programmatic integrations still use shared page/app-level SDK state (setEnvironment, setPageVisit, setChannel, setConsent, setDeviceId, setUserId, setContextItem, setContextItemType), while list/filter/request arguments are supplied per API call.

Need a full runnable project for this mode? Use frontend-sdk-examples for programmatic JS/TS and retail-oriented examples.

How it works:

  1. Configure SDK state (setEnvironment, setPageVisit, consent, identity).

  2. Track page-level or business events explicitly with the dedicated helper functions when available, or with sendEvent(…​) for custom event types.

  3. Request recommendation items (getRecommendations, smartSort, or proxy flow).

  4. Render with your own UI code.

  5. Track recommendation-related events explicitly when your UI owns rendering and the SDK cannot infer attribution from SDK-rendered nodes.

Request Models

The SDK exposes two request models:

API / integration shape Model Result shape

getRecommendations(…​), smartSort(…​), useRecoList(…​), useSmartSort(…​)

List-based

One call or hook returns one or more recommendation lists, and each list can contain multiple items.

proxyReco(…​), useCreateReco(…​), declarative data-froomle-reco placeholders

Slot-based

One proxy, hook call, or placeholder represents one rendered recommendation item. The SDK batches compatible slots into backend list requests automatically.

Use the list-based model when you want to handle arrays of items directly in your own code. Use the slot-based model when recommendations are rendered as repeated cards/placeholders and you want the SDK to batch compatible requests automatically.

getRecommendations

import { getRecommendations } from '@froomle/frontend-sdk'

const response = await getRecommendations([
  {
    list_name: 'recommended_for_you',
    limit: 4,
    list_size: 4
  }
])

const items = response.items()

Type note: list entries are typed with required limit, list_name, and list_size, plus optional additional keys typed as [key: string]: unknown.

Response item shape:

  • response.items() returns RecommendationItem[].

  • Each RecommendationItem always includes:

    • Id: string

    • RequestId: string

    • UserGroup: string

  • Content fields are model/backend-dependent and are accessed via item.get(key). There is no single fixed payload schema for these dynamic keys.

for (const item of response.items()) {
  const id = item.Id
  const requestId = item.RequestId
  const userGroup = item.UserGroup

  // Dynamic payload fields depend on your recommendation setup.
  const title = item.get('title')
  const uri = item.get('uri')
  const image = item.get('image') ?? item.get('images')?.[0]
}

Error handling:

  • getRecommendations(…​) returns a promise and rejects on request/HTTP/response-parse failures.

  • Handle with try/catch (or .catch(…​)) in programmatic flows.

try {
  const response = await getRecommendations([
    { list_name: 'recommended_for_you', limit: 4, list_size: 4 }
  ])
  // use response
} catch (error) {
  // network / HTTP / parse failure
}

Need full request payload rules (required fields, list structure, histories, batching)? See Recommendation requests.

Smart Sorting and Reranking with the SDK

Smart sorting now has a dedicated SDK page because the integration contract differs by rendering method. See Smart Sorting and Reranking with the SDK for declarative DOM/script-tag, module/import DOM, programmatic JS/TS, React, backend-fetched, diagnostics, and troubleshooting guidance.

proxyReco + fulfillRecommendations

Queue multiple slot requests first, then resolve them in one batch call.

This is the same slot-based model used by declarative markup and React helpers:

  • one proxyReco(…​) call represents one rendered item

  • not one returned list

  • compatible queued proxies are merged into one backend request when fulfillRecommendations() runs

import { proxyReco, fulfillRecommendations } from '@froomle/frontend-sdk'

const first = proxyReco({ list: 'recommended_for_you', filters: [] })
const second = proxyReco({ list: 'recommended_for_you', filters: [] })

await fulfillRecommendations()
const [itemA, itemB] = await Promise.all([first, second])

Error handling:

  • proxyReco(…​) returns a promise-like recommendation handle; it does not resolve to null.

  • Each proxy resolves to a RecommendationItem or rejects.

  • Rejection cases include:

    • batch request failure in fulfillRecommendations()

    • fewer returned items than queued requests (No recommendation available)

const first = proxyReco({ list: 'recommended_for_you', filters: [] })
const second = proxyReco({ list: 'recommended_for_you', filters: [] })

try {
  await fulfillRecommendations()
} catch (error) {
  // batch request failed; queued proxies reject
}

const results = await Promise.allSettled([first, second])

Manual Event Helpers

Use the dedicated helper functions for the common manual/programmatic flows the SDK now models explicitly.

import {
  sendAddToCart,
  sendItemInteraction,
  sendPurchase,
  sendRemoveFromCart,
  sendUserInteraction
} from '@froomle/frontend-sdk'

// Uses current context_item / context_item_type from SDK state
sendItemInteraction('product_like')

sendUserInteraction('submit_review')

sendAddToCart(2, {
  basket_content: [{ id: 'product-111' }, { id: 'product-987' }]
})

sendRemoveFromCart(1, {
  basket_content: [{ id: 'product-111' }]
})

sendPurchase(2, 19.99, {
  original_price: 29.99
})
  • These helper functions use the same runtime defaults as sendEvent(…​): page_type, channel, device_id, user_id, and user_group are read from SDK state when available.

  • For item-based helpers (sendItemInteraction, sendAddToCart, sendRemoveFromCart, sendPurchase), omit actionItem / actionItemType to reuse the current context_item / context_item_type.

  • If you pass explicit actionItem / actionItemType, those values win over context.

  • If an item-based helper has neither explicit item args nor current context, it does not emit an event.

  • Use these helpers for item_interaction, user_interaction, add_to_cart, remove_from_cart, and purchase.

  • Keep sendEvent(…​) for unsupported/custom event types or when you need total payload control.

Retail Trigger-Based Events

For retail flows, wire the dedicated helper functions directly to the business trigger in your UI code. Do not rely on generic click tracking for these events.

Typical examples:

  • sendAddToCart(…​) from an Add to Cart button or cart form submit.

  • sendRemoveFromCart(…​) from a remove-line action in the basket.

  • sendPurchase(…​) from the confirmed checkout or thank-you step, not from the initial buy button.

  • sendItemInteraction(…​) for actions such as wishlist, favorite, or compare.

  • sendUserInteraction(…​) for non-item flows such as review submit, quote request, or starting a chat.

import {
  sendAddToCart,
  sendItemInteraction,
  sendPurchase,
  sendRemoveFromCart,
  sendUserInteraction,
  setContextItem,
  setContextItemType,
  setPageVisit,
  setChannel
} from '@froomle/frontend-sdk'

setPageVisit('product_detail')
setChannel('www-desktop')
setContextItem('32636759')
setContextItemType('product')

buttonAddToCart.onclick = () => {
  sendAddToCart(2, {
    basket_content: [{ id: '32636759' }],
    merchandising_slot: 'product-detail'
  })
}

buttonWishlist.onclick = () => {
  sendItemInteraction('product_like', {
    source: 'product-detail'
  })
}

buttonReview.onclick = () => {
  sendUserInteraction('submit_review', {
    review_surface: 'product-detail'
  })
}

buttonRemoveFromCart.onclick = () => {
  sendRemoveFromCart('32636759', 'product', 1, {
    basket_content: []
  })
}

confirmPurchase.onclick = () => {
  sendPurchase(1, 119.99, {
    original_price: 149.99,
    checkout_id: 'checkout-2048'
  })
}

Retail helper guidance:

  • Prefer the dedicated helper matching the business action instead of raw sendEvent(…​).

  • Set context_item / context_item_type on product-detail pages when you want the short helper forms.

  • Use explicit item args when the triggered item differs from the current page context, such as a basket line or variant selector.

  • extras is merged last, so add flow-specific fields such as basket_content, original_price, checkout_id, or merchandising_slot there.

  • For recommendation-related attribution fields (list_name, request_id, user_group), keep using sendEvent(…​) or pass those fields explicitly when your custom flow requires them.

sendEvent

Use sendEvent(…​) when you need explicit control over an event type or payload that is not covered by the dedicated helper functions.

import { sendEvent } from '@froomle/frontend-sdk'

sendEvent('sample_item_id_1', 'article', 'impression', {
  list_name: 'recommended_for_you',
  request_id: 'req-id'
})

When you already have a rendered recommendation or control element, you can also let the SDK infer recommendation metadata from that element:

sendEvent(undefined, undefined, 'click_on_recommendation', undefined, {
  source: clickedElement
})

Need the full event contract (event types, required fields, and delivery guidance)? See Tracking events.

  • You normally should not need sendEvent(…​) for standard e-commerce triggers. For first-party support of retail flows, use the dedicated helpers from Retail Trigger-Based Events instead: sendItemInteraction(…​), sendUserInteraction(…​), sendAddToCart(…​), sendRemoveFromCart(…​), and sendPurchase(…​).

  • sendEvent auto-populates base fields from SDK runtime state: event_type, page_type, channel, device_id, and (when available) user_id and user_group.

  • action_item / action_item_type come from the call, but if the runtime receives undefined values in JS it falls back to context_item / context_item_type when present.

  • page_visit is page-level. It does not require action_item / action_item_type, so manual programmatic integrations can call sendEvent(undefined, undefined, 'page_visit') (or null in plain JS) after setting page_type through SDK state.

  • extras is merged last. Use it to add fields such as list_name or request_id, or to intentionally override default fields (for example page_type / channel) in custom flows.

  • options.source can infer recommendation attribution from the nearest rendered node: action_item, action_item_type, list_name, request_id, and user_group. This is especially useful for benchmarked control-vs-Froomle placements where the UI already has the resolved DOM node.

  • Explicit call arguments and explicit extras values still win over anything inferred from options.source.

  • Programmatic/manual event delivery uses navigation-safe transport defaults in the browser (keepalive with beacon fallback when needed). You do not pass transport flags through extras.

  • Script-tag attributes are bootstrap inputs for SDK state; they are not per-event arguments to sendEvent(…​).

  • For programmatic integrations, event payloads and event types must follow Tracking events.

extras requirement guide:

Field in extras Requirement

list_name, request_id

For recommendation-related events (impression, click_on_recommendation), treat these as required for reliable attribution/reporting. For non-recommendation events, they are optional.

user_group

Required for recommendation-related events. The SDK auto-populates it when available from runtime/DOM state; pass it explicitly in custom flows when that state is unavailable.

user_id

Usually inferred automatically from SDK state when consent allows and a user_id has been set. Pass it explicitly only when you intentionally want to override the current SDK user context.

page_type, channel, device_id

Optional overrides only. The SDK already sets these from runtime state.

React Bindings

React helpers are exposed from @froomle/frontend-sdk/react. Use this layer when your recommendation blocks are React components instead of DOM templates with data-froomle-*.

For benchmark-specific React shapes (benchmark.controlItem, benchmark.controlItems, entries), see Benchmarking and A/B Testing with the SDK.

For setter scope, see Runtime Scope Guide. React integrations still use shared page/app-level SDK state (setEnvironment, setPageVisit, setChannel, setConsent, setDeviceId, setUserId, setContextItem, setContextItemType), while each recommendation block/component supplies its own list/filter/rendering configuration.

Need a complete React example app instead of API snippets? Use frontend-sdk-examples. The examples repo includes React news, retail, and ArcXP examples.

React bindings are intended for the client-rendered / rehydrated React layer of your app.

  • Use this pattern in a browser entry such as main.jsx, or in a framework-specific client bootstrap that runs under the real React app root.

  • Do not mount FroomleSdkInit from a server-only shell, output type, or SSR template wrapper that never rehydrates on the client.

  • React bindings do not call init() and do not start declarative DOM placeholder mode.

  • Raw declarative data-froomle-* blocks injected onto a React page stay idle unless you explicitly start DOM placeholder mode yourself. Mixing those modes on the same page is not the preferred integration path.

  • In hybrid frameworks, browser-persistent setters such as setConsent(…​), setUserId(…​), and setDeviceId(…​) must run in browser context unless your host platform sets equivalent cookies itself on the server response.

  • If you only need shell-level integration and automatic SDK-managed events (page_visit, detail_pageview, impression, click_on_recommendation), prefer Script-Tag Setup and the runnable plain HTML examples in frontend-sdk-examples instead of forcing the React bindings into the server shell.

ArcXP / Fusion

In ArcXP / Fusion, use the following runtime model:

  • the Output Type is the server-rendered shell

  • the client-rendered / rehydrated React layer runs under #fusion-app

Recommended ArcXP integration:

  • Initialize page/app-level SDK state in the client-rendered React layer.

  • Mount FroomleSdkInit there.

  • Render recommendation UI with useCreateReco or useRecoList, together with FroomleReco / useReco, there.

  • Keep the Output Type limited to server-shell responsibilities.

  • If a page only needs automatic page_visit and no recommendations yet, still mount FroomleSdkInit on that page and set setPageVisit(…​). No recommendation component is required.

  • If a detail page only needs automatic detail_pageview and no recommendations, still mount FroomleSdkInit on that page and set setPageVisit(…​), setContextItem(…​), and optional setContextItemType(…​). No recommendation component is required for that page.

  • A homepage-only React integration does not automatically give destination article pages their own detail_pageview; the destination detail page still needs its own lightweight SDK bootstrap.

The following setters define page/app-level runtime state and are normally set once per page context, not once per recommendation block:

  • setEnvironment(…​)

  • setPageVisit(…​)

  • setChannel(…​)

  • setConsent(…​)

  • setDeviceId(…​)

  • setUserId(…​)

  • setContextItem(…​)

  • setContextItemType(…​)

If a page contains multiple recommendation blocks, set these values once for the current page context and render the blocks against that shared SDK state. If the page context changes during client-side navigation, update the page-level setters once for the new route/context.

ArcXP integration options:

Path Use when Notes

React bindings in the client-rendered layer under #fusion-app

Recommendations are part of the React app itself.

Preferred ArcXP integration. Use @froomle/frontend-sdk/react, mount FroomleSdkInit in the browser layer, and render one or more recommendation blocks from shared page-level SDK state.

Script-tag/static shell

Recommendation markup remains server-rendered and is not rehydrated on the client.

Narrow exception only. Use this in the Output Type shell or in a feature explicitly marked .static = true. Do not use this for normal rehydrated ArcXP feature trees.

Use frontend-sdk-examples for runnable examples. The examples repo includes an ArcXP React example (arcxp-native-react-example) and a static-shell exception example (arcxp-native-static-example).

Mental model:

  1. Configure SDK once in the client entry (main.jsx in this example).

  2. Wrap the app with FroomleSdkInit.

  3. Choose one React rendering paradigm for each recommendation flow.

  4. Render each recommendation item through FroomleReco when you want automatic recommendation-item event tracking.

  5. Add advanced composition with FroomleOrder and FroomleCustomItem.

import React from 'react'
import ReactDom from 'react-dom/client'
import { setEnvironment, setPageVisit, setConsent } from '@froomle/frontend-sdk'
import { FroomleSdkInit } from '@froomle/frontend-sdk/react'
import App from './App'

setEnvironment('sample_env')
setPageVisit('home')
setConsent(2)

ReactDom.createRoot(document.getElementById('root')).render(
  <FroomleSdkInit>
    <App />
  </FroomleSdkInit>
)

Core React primitives:

  • useCreateReco({ list, filters })

  • useRecoList({ list, filters, limit, list_size })

  • useSmartSort({ listName, candidates, getId })

  • useRecoEntry({ list, benchmark })

  • FroomleReco

  • useReco

  • FroomleOrder and FroomleOrderItem

  • FroomleCustomItem

Supported React Paradigms

Paradigm Use when Behavior

Slot-based rendering with useCreateReco(…​)

One rendered card or slot maps to one recommendation item.

One hook call returns one recommendation handle. Compatible hook calls are batched into backend list requests automatically.

List-based rendering with useRecoList(…​)

A parent component owns one recommendation request, transforms the returned list once, and then passes data to presentational children.

One hook call returns { status, loading, error, response, list, items, entries }. Returned Froomle items can be rendered inside FroomleReco reco={item}; benchmark entries can be rendered inside FroomleReco entry={entry}.

Entry-based benchmark rendering with useRecoEntry(…​)

One rendered card or slot should support raw customer/control content in a benchmarked placement.

One hook call returns one branch-aware entry, or null if no usable entry exists. Render the entry through FroomleReco entry={entry}.

Smart sorting with useSmartSort(…​)

Your component already has candidate items, such as search results, and needs Froomle to rerank them.

One hook call sends candidates as list_content, returns sorted candidate entries, and keeps automatic recommendation tracking when entries are rendered through FroomleReco entry={entry}.

React supports both paradigms, but they are not interchangeable:

  • useCreateReco(…​) is slot-based. One hook call returns one recommendation item, not a list of items.

  • useRecoList(…​) is list-based. It returns all items for one list request, but automatic impression and click_on_recommendation tracking still depends on rendering each returned item through FroomleReco.

  • useRecoEntry(…​) is entry-based. It exists for single-slot raw-item benchmarking and should be paired with FroomleReco entry={entry}.

  • useSmartSort(…​) is list-based reranking. It sends caller-provided candidates as list_content and should be paired with FroomleReco entry={entry} when automatic recommendation tracking is required.

  • If you render raw markup from useRecoList(…​) or useSmartSort(…​) without FroomleReco, treat impression and click_on_recommendation as a manual responsibility.

  • Repeated React filter keys merge into arrays on the outgoing list request.

  • Custom variables keep last-write-wins semantics; pass an explicit array value if a custom field is intentionally multi-valued.

React Component Behavior

useCreateReco

useCreateReco(…​) returns a recommendation handle, not just a settled item.

Return value:

  • Type: RecommendationItem & PromiseLike<RecommendationItem>.

  • You pass this handle to <FroomleReco reco={…​}>.

  • The same handle also exposes recommendation fields (for example reco.uri) once resolved.

  • Dynamic keys can always be read with reco.get('field_name').

Practical guidance:

  • One useCreateReco(…​) call represents one recommendation item, not a list of items.

  • If you want 4 recommendation cards, render 4 hook calls / 4 FroomleReco wrappers.

  • Compatible useCreateReco(…​) calls are batched into one backend request automatically.

  • Repeated filter keys merge into arrays on that grouped request. Example: two categories filters become "categories": ["Monde", "Sports"].

  • If you want the clearest render flow, read recommendation fields via useReco() inside a FroomleReco subtree.

  • Direct field access on the handle is supported, but those fields are empty until the reco resolves.

useRecoList

useRecoList(…​) returns a resolved recommendation list for parent-owned rendering flows.

Return value:

  • Type: { status, loading, error, response, list, items, entries }.

  • items is the public Froomle-item render list. Without benchmark config, and on treatment/Froomle benchmark branches, it contains returned Froomle recommendation items. On customer/control benchmark branches, it is empty so returned Froomle items are not rendered accidentally.

  • entries is the branch-aware render list for SDK-managed benchmark rendering. On customer/control branches it contains usable raw control items when you provide benchmark.controlItems; otherwise it is empty. On treatment/Froomle branches it contains returned Froomle items, mapped when configured.

  • list is the resolved public list object (or null while loading / on error). Its items follows the same branch-aware rule as the top-level items.

  • response is the full raw response object (or null while loading / on error), for inspection/debugging.

Practical guidance:

  • Use this when a parent component wants one list request and then maps the returned items into presentational children.

  • limit and list_size remain caller-configurable, for example from CMS settings.

  • exclusions, excludeItems, and autoExclusions can be passed on the hook request when this list must avoid items already visible elsewhere on the page. See Item IDs and Histories.

  • Use excludeItems when your React/CMS component already has the visible item objects in memory. It maps { items, getId, itemType/getItemType } to request-level exclusions without adding wrapper DOM or relying on a page scan. TypeScript infers the item shape from items.

  • Returned items can still be rendered inside FroomleReco for normal and treatment/Froomle rendering, so automatic impression and click_on_recommendation tracking continues to work.

  • useRecoList(…​) is list-based. It does not rely on the slot-batching behavior of useCreateReco(…​).

  • Repeated filter keys merge into arrays on the outgoing list request.

  • The React hook returns runtime objects. You do not import generated RecommendationList or Recommendations constructors from the root SDK package.

useSmartSort

useSmartSort(…​) returns a smart-sorted candidate list for parent-owned rendering flows.

Return value:

  • Type: { status, loading, error, response, list, items, entries, sortedCandidates, unmatchedCandidates }.

  • entries contains matched candidate/Froomle item pairs for rendering through FroomleReco entry={entry}.

  • sortedCandidates contains the original candidates in Froomle response order.

  • unmatchedCandidates contains candidates that were not returned by Froomle.

Practical guidance:

  • Use this for search-result reranking, curated-list reranking, or other cases where the application already owns the candidate set.

  • Candidate IDs can come from id, item_id, itemId, or getId(candidate, index).

  • Candidate item type defaults to article; set itemType / item_type or getItemType(candidate, index) for products or other item types.

  • Use getRank(candidate, index) or candidate rank for fixed-rank placements.

  • benchmark, exclusions, excludeItems, and autoExclusions follow the same request-shaping rules as useRecoList(…​).

useRecoEntry

useRecoEntry(…​) returns one branch-aware benchmark entry for single-slot raw-item rendering.

Return value:

  • Type: FroomleRecoEntry<TItem> | null.

  • entry.item is the item your component should render.

  • entry.branch is "control" or "treatment".

  • Render the entry through <FroomleReco entry={entry}> so the SDK can stamp recommendation attribution metadata.

Practical guidance:

  • Use this for one-card benchmark slots where the control branch should render one raw customer/CMS item.

  • Put the raw item adapter inside benchmark.controlItem.

  • exclusions, excludeItems, and autoExclusions follow the same request-shaping rules as useRecoList(…​).

  • Use useCreateReco(…​) instead when you only want standard returned Froomle recommendation content.

FroomleReco

FroomleReco is a wrapper component around a single recommendation handle.

Props:

  • reco: recommendation handle from useCreateReco(…​), or a resolved RecommendationItem from useRecoList(…​).items.

  • entry: branch-aware entry from useRecoList(…​).entries, useSmartSort(…​).entries, or useRecoEntry(…​).

  • Pass either reco or entry, not both.

  • children: render content.

  • asChild: optional. When set, FroomleReco preserves a single child element as the rendered DOM root instead of inserting its default wrapper <div>. The child must be a real DOM element, or a component that forwards props and ref to its root DOM element.

  • Any extra props (id, className, data-*, …​) are forwarded to the rendered root element.

Runtime behavior:

  • By default, renders a <div> wrapper and places children inside it.

  • With asChild, clones a single child element and uses that child as the rendered DOM root.

  • Resolves reco or reads entry metadata and writes recommendation metadata on the rendered root element: data-froomle-reco (when list is known), data-froomle-id, data-froomle-item-type, data-froomle-request-id, data-froomle-user-group.

  • Provides the reco value or entry.item through React context for useReco().

  • Use asChild when your layout expects the article/card root itself to remain the direct grid or flex child.

Choosing between the two modes:

  • Use the default wrapper mode when you want the broadest compatibility and your child content is not a single stable DOM root.

  • Use asChild when the recommendation card/article root itself must remain the actual DOM node for layout, grid, or flex behavior.

  • asChild requires a single child element. That child must either be a real DOM element, or a component that forwards props and ref to its root DOM element. It is the preferred mode for layout-sensitive card rails.

  • If an existing card component does not forward props / ref, add a thin adapter that renders the real <article> / <div> root and passes the Froomle props to that root.

useReco() behavior:

  • Returns the current recommendation from the nearest FroomleReco provider.

  • Throws if called outside a FroomleReco subtree.

FroomleCustomItem

FroomleCustomItem is for manually inserted editorial/custom items that should still influence recommendation history.

Props:

  • id: item id to add to histories.

  • children: rendered content.

Runtime behavior:

  • Renders children as-is in a fragment (no extra DOM wrapper).

  • Calls addHistories([id]) once per mount.

  • Does not fetch recommendations.

  • Does not inject recommendation attributes or clone children.

Example usage:

import {
  FroomleReco,
  FroomleOrder,
  FroomleOrderItem,
  FroomleCustomItem,
  useCreateReco,
  useReco
} from '@froomle/frontend-sdk/react'

function RecoLink() {
  const reco = useReco()
  return <a href={reco.get('uri')}>{reco.get('title')}</a>
}

function Example() {
  const recoHandle = useCreateReco({ list: 'recommended_for_you', filters: [] })

  return (
    <>
      <FroomleReco reco={recoHandle} asChild>
        <article className="news-card">
          <RecoLink />
        </article>
      </FroomleReco>

      <FroomleOrder list="recommended_for_you" category="categories">
        <FroomleOrderItem value="Sports"><section>Sports</section></FroomleOrderItem>
        <FroomleOrderItem value="Monde"><section>World</section></FroomleOrderItem>
      </FroomleOrder>

      <FroomleCustomItem id="sample_item_id_1">
        <article>Editorial item</article>
      </FroomleCustomItem>
    </>
  )
}

Adapter example for an existing card component:

import { forwardRef } from 'react'

function ArticleCard({ article }) {
  return <a href={article.uri}>{article.title}</a>
}

const RecoArticleRoot = forwardRef(function RecoArticleRoot(
  { article, className = '', ...props },
  ref
) {
  return (
    <article ref={ref} className={className} {...props}>
      <ArticleCard article={article} />
    </article>
  )
})

function ParentMappedRecoArticle({ article, item }) {
  return (
    <FroomleReco reco={item} asChild>
      <RecoArticleRoot article={article} />
    </FroomleReco>
  )
}

List-based example with a preserved article root:

function RecoArticle() {
  const reco = useReco()

  return (
    <a href={reco.get('uri')}>
      {reco.get('title')}
    </a>
  )
}

function RecoListExample() {
  const { items, loading } = useRecoList({
    list: 'recommended_for_you',
    filters: [],
    limit: 4,
    list_size: 4
  })

  if (loading) {
    return null
  }

  return items.map((item, index) => (
    <FroomleReco reco={item} asChild key={index}>
      <article className="news-card">
        <RecoArticle />
      </article>
    </FroomleReco>
  ))
}

This pattern keeps the parent request list-oriented while preserving the article/card root as the real DOM element for layout and styling.

function ParentMappedRecoArticle({ article, item }) {
  return (
    <FroomleReco reco={item} asChild>
      <article className="news-card">
        <a href={article.uri}>{article.title}</a>
      </article>
    </FroomleReco>
  )
}

Benchmarking and A/B Testing with the SDK

This section explains how to implement the benchmark modes from AB testing and benchmarking with the frontend SDK.

Start with the guide page to agree the benchmark contract first:

  • who assigns the split

  • whether control content lives inside the benchmarked placement

  • which group names are in use

  • whether customer/control content is rendered by your application or provided inside an SDK-managed placement

If those decisions are unclear, the SDK code is usually not the real blocker.

Benchmark orchestration belongs to the recommendation implementation phase, not to the earlier page/business event rollout phase.

Choose the benchmark shape before you implement the recommendation placement itself, because that is where you decide:

  • whether the SDK or the host application owns the control branch

  • whether the branch decision happens inside SDK-managed rendering or in your own UI code

  • whether recommendation impression / click_on_recommendation tracking can stay automatic, or becomes partly manual

If you are unsure which benchmark shape fits your use case, contact your Froomle account team before implementing the placement. They can help you design the benchmark contract, choose the right ownership model, and avoid rework later in the integration.

Shared SDK Model

These rules apply across DOM, JS/TS, and React:

  • user_group is request- and placement-scoped, not page-scoped.

  • setBenchmark(…​) and script-tag data-froomle-benchmark-* inputs define default benchmark config only. A per-request or per-placement override is more specific.

  • DOM benchmark overrides merge field-by-field. A placement-level data-froomle-benchmark-* value overrides the script/global default for that field, while unspecified fields inherit from the global default.

  • getRecommendations(…​, { benchmark }) applies one benchmark config to that whole call. If different lists need different benchmark configs, use separate calls.

  • proxyReco(…​), useCreateReco(…​), and useRecoList(…​) resolve benchmark config per proxied request or per hook call.

  • Requests only batch together when their effective benchmark configs are equal. Different effective benchmark configs split into separate outgoing recommendation requests.

  • Sequential order still matters when grouping. An A, B, A order becomes three outgoing groups, not one merged A group around B.

  • Under consent level 0 or 1, the effective benchmark config still decides request-root user_group: no benchmark or froomle-managed benchmark keeps user_group: "no-consent"; customer-managed customer/control keeps the configured control user_group; customer-managed treatment/Froomle omits request-root user_group. In all cases, anonymous recommendation identity still uses device_id: "no-consent" and version: "no-consent".

  • page_visit and detail_pageview remain page-context events. They use global customer-managed benchmark attribution when a global customer/control group is configured, but they do not infer per-placement benchmark overrides from individual recommendation blocks or React hooks.

  • request_id always comes from the Froomle response. You do not invent it.

  • data-froomle-id stays the generic item identity for histories and content attribution. Recommendation auto-tracking activates only after the SDK also has resolved recommendation request metadata.

  • data-froomle-reco is list/placement identity. It is not item identity.

  • Benchmark config decides the branch (user_group). It does not identify the customer-rendered item shown on the control branch.

  • The resolved user_group is authoritative for rendering. A customer/control branch must not render returned Froomle items by default. A treatment/Froomle branch renders returned Froomle items, or empty if the response contains no usable items.

  • For SDK-managed control tracking, the control item must have stable item identity before the SDK can stamp returned request metadata (request_id, user_group) onto that rendered item.

  • window.FroomleFrontendSdkRuntime.diagnostics.benchmark is the support-facing place to inspect effective benchmark defaults, the last resolved benchmark placement, and recent benchmark warnings. It is observational state, not a control surface.

When the SDK Can Fully Own the Benchmark

The SDK can automatically preserve or replace content and auto-track both branches only when the control content lives inside the placement the SDK manages.

Examples of SDK-managed placements:

  • a declarative DOM placeholder (data-froomle-reco="…​") whose existing control card also has stable data-froomle-id

  • a React useRecoList(…​) placement where benchmark.controlItems passes raw CMS items together with getId(…​) / getItemType(…​) adapters

  • a React useRecoEntry(…​) slot where benchmark.controlItem passes one raw CMS item together with getId(…​) / getItemType(…​) adapters

If the control content lives outside the SDK-managed placement, the SDK still supports benchmark request shaping and Froomle item rendering, but your app still owns:

  • the control UI

  • the branch decision in your own render code

  • manual control-branch tracking where required

Customer-managed split is not the same thing as SDK-managed control tracking.

The SDK can send user_group: "customer" correctly and still not be able to auto-track the visible customer cards if those cards do not expose stable item identity. In declarative DOM/script-tag mode, that identity is data-froomle-id on each customer/control card.

Branch-aware vs Control-aware Benchmarking

There are two different SDK shapes, and you should choose deliberately:

Shape What you provide to the SDK Runtime behavior

Branch-aware benchmark request

Only benchmark config. No explicit SDK-owned control element or control item is provided. The diagnostics variant value for this shape is response-driven for compatibility.

The SDK still follows the resolved user_group. On the treatment/Froomle branch it renders returned Froomle items. On the customer/control branch it does not render returned Froomle items; your application must provide/keep the customer content outside this SDK-owned contract and own control-branch tracking where needed.

Control-aware benchmarking

Benchmark config plus an SDK-owned control element or control item with stable item identity. In React, control items are raw render items passed inside benchmark.controlItem / benchmark.controlItems; provide getId(…​) and optional getItemType(…​) adapters so the SDK can attribute them.

The SDK can preserve your control branch, replace it on the treatment branch, stamp resolved request metadata, and auto-track both branches as recommendation placements. On the control branch, usable customer/control content wins over returned recommendation items. If control content is missing or unusable, returned Froomle items are ignored by default and the SDK warns.

Use branch-aware benchmark requests without control items when you only need to validate request shaping and returned user_group first. Use control-aware benchmarking when you want the SDK to own the control-vs-Froomle rendering decision inside the placement itself.

Branch rendering matrix:

Resolved branch Returned Froomle items SDK-rendered result

Customer/control

Non-empty

Render customer/control content if the integration provided it. Ignore returned Froomle items by default.

Customer/control

Empty

Render customer/control content if the integration provided it. Otherwise render nothing / keep the existing empty shell and warn where applicable.

Treatment/Froomle

Non-empty

Render returned Froomle items.

Treatment/Froomle

Empty

Render empty.

Benchmark Config Shape

The shared benchmark config shape is:

{
  mode?: "froomle-managed" | "customer-managed"
  currentGroup?: string | null
  groups?: {
    control?: string
    treatment?: string
  }
}

Normalization rules:

  • mode defaults to froomle-managed when benchmark config exists but mode is omitted.

  • groups.control defaults to customer.

  • groups.treatment defaults to froomle.

  • currentGroup is required for customer-managed split before the request is sent.

Use benchmark config only when you actually want benchmark-aware behavior. If you are only rendering standard returned recommendations, you can keep using the SDK without any benchmark config.

React control-aware benchmarking uses raw-item control adapters inside the benchmark object:

type BenchmarkControlItem<TControl> = {
  item: TControl
  getId: (item: TControl, index: number) => string
  getItemType?: (item: TControl, index: number) => string
}

type BenchmarkControlItems<TControl> = {
  items: readonly TControl[]
  getId: (item: TControl, index: number) => string
  getItemType?: (item: TControl, index: number) => string
}

Rules:

  • getId(…​) returns the stable item identity used for recommendation attribution.

  • getItemType(…​) defaults to article when omitted.

  • The raw item can be your CMS article/product object. It does not need to be converted into a Froomle recommendation item.

  • The raw item must still contain the fields your React card renderer reads, for example title, URL, image, label, or publish date.

  • Passing only IDs is not enough for SDK-managed React control-aware rendering because the SDK cannot render your control card from an ID alone. IDs alone are only enough when your application owns rendering and sends recommendation tracking metadata itself, or when declarative DOM control content already exists in the page and only needs stable item identity.

The Three Benchmark Modes

At the product level, the SDK needs to explain these three cases clearly:

Mode Who assigns the group? What the request looks like What the frontend should expect

Froomle-managed split, all groups are Froomle-rendered

Froomle

Standard recommendation request. No explicit user_group.

Render the returned Froomle items. This is standard SDK behavior and does not require control content.

Froomle-managed split, customer vs Froomle

Froomle

Standard recommendation request. No explicit user_group.

Froomle chooses the group and the SDK follows the response user_group. Customer/control renders customer content when provided, regardless of whether the response contains returned items. Treatment/Froomle renders returned Froomle items, or empty if the response is empty.

Customer-managed split

Customer

Benchmark-aware request. Customer/control sends request-root user_group; treatment/Froomle omits request-root user_group.

The SDK follows the chosen branch. Rendering follows the same branch matrix: customer/control renders customer content when provided, treatment/Froomle renders returned Froomle items.

The important consequence is:

  • the SDK supports all three business modes

  • but you still need to choose whether customer/control content is provided inside the SDK-managed placement, or rendered/tracked by your application outside it

Declarative DOM / Script-Tag Integrations

Best fit for:

  • CMS-driven pages

  • server-rendered templates

  • JSP or similar SSR output

  • control cards that already live in the same DOM node as the recommendation slot

Option A: Froomle-managed, model vs model

No special benchmark config is required. Use normal data-froomle-reco placeholders and render the returned items.

Option B and Customer-managed Split: Control-aware Placements

If you want the SDK to preserve control content on the control branch and replace it on the treatment branch:

  1. Enable benchmark config on the SDK script or on the placement itself.

  2. Keep the control content inside the same data-froomle-reco element.

  3. Give every trackable control item a stable data-froomle-id.

  4. Optionally set data-froomle-item-type when the control item is not an article.

<script
  defer
  src="https://cdn.jsdelivr.net/npm/@froomle/frontend-sdk@latest/dist/froomle.global.js"
  data-froomle-env="sample_env"
  data-froomle-page-visit="home"
  data-froomle-benchmark-mode="customer-managed"
  data-froomle-benchmark-current-group="customer"
></script>

<article
  class="headline-card"
  data-froomle-reco="homepage_headline"
  data-froomle-id="cms-article-123"
  data-froomle-item-type="article"
>
  <a href="/editorial/cms-article-123">
    <h3>Editorial control headline</h3>
  </a>
</article>

Runtime behavior for that placement:

  • treatment branch: the SDK fills the node with the returned recommendation item

  • control branch with usable control content: the SDK keeps your control content and stamps runtime recommendation metadata onto that same node

  • control branch with missing or unusable control content: the SDK ignores returned Froomle items, keeps the node as rendered, and warns

  • if control content is missing or unusable, automatic recommendation attribution cannot activate unless the rendered item has stable identity and resolved request metadata

What counts as usable control content in DOM mode:

  • the placement has data-froomle-id

  • and it contains actual renderable content, not just an empty shell

data-froomle-reco alone is not enough for customer/control attribution. It tells the SDK which list this placement belongs to, but it does not tell the SDK which article/product was shown. Without data-froomle-id, the SDK cannot create an impression or click_on_recommendation event with a reliable action_item.

Incorrect for automatic control-branch tracking:

<li data-froomle-reco="article_similar">
  <a href="/articles/sample-article-123">Customer baseline article</a>
</li>

Correct for automatic control-branch tracking:

<li
  data-froomle-reco="article_similar"
  data-froomle-id="sample-article-123"
  data-froomle-item-type="article"
>
  <a href="/articles/sample-article-123">Customer baseline article</a>
</li>

On the control branch, the SDK can then keep that content and add returned request metadata such as:

data-froomle-request-id="..."
data-froomle-user-group="customer"

Per-placement overrides use the same attribute names:

  • data-froomle-benchmark-mode

  • data-froomle-benchmark-current-group

  • data-froomle-benchmark-control-group

  • data-froomle-benchmark-treatment-group

Use these when most placements share one page-level default but one placement needs different benchmark behavior.

Programmatic Integrations (JS/TS)

Best fit for:

  • headless or custom-rendered frontends

  • direct getRecommendations(…​) usage

  • apps that already own their rendering and only need SDK request shaping and tracking helpers

Available API:

import {
  clearBenchmark,
  getBenchmark,
  getRecommendations,
  setBenchmark
} from '@froomle/frontend-sdk'

setBenchmark(…​) defines a default benchmark config for later requests. Per-request benchmark config can override that default:

setBenchmark({
  mode: 'customer-managed',
  currentGroup: 'customer',
})

const response = await getRecommendations(
  [
    { list_name: 'homepage_headline', limit: 3, list_size: 3 }
  ],
  {
    benchmark: {
      mode: 'customer-managed',
      currentGroup: 'froomle',
    }
  }
)

Request-shaping rules:

  • customer-managed customer/control branch: the SDK injects request-root user_group

  • customer-managed treatment/Froomle branch: the SDK omits request-root user_group

  • froomle-managed split: the SDK does not inject request-root user_group

  • requests with different effective benchmark configs do not batch together

Programmatic rendering note:

  • Bare JS/TS programmatic integrations always own rendering themselves.

  • The SDK can shape requests and help with tracking, but it does not own an automatic DOM preservation layer in this mode.

  • Use the response user_group as the branch authority: customer/control should render your customer content, treatment/Froomle should render returned Froomle items.

  • If your control branch is external UI outside the SDK-managed placement, the branch rendering and tracking are still your responsibility.

Customer vs Froomle in Bare JS/TS

In bare JS/TS programmatic mode there is no SDK-owned overridable element contract.

That means:

  • Froomle-managed Option A: fully supported, render returned items

  • customer/control vs Froomle: supported, but you own the branch rendering and must not render returned Froomle items on the customer/control branch unless you deliberately implement a custom fallback outside the SDK contract

  • control-aware control vs Froomle with SDK-managed replacement: not a native bare-JS DOM abstraction; use declarative DOM or React if you want the SDK to own that replacement behavior

If you stay in bare JS/TS:

  • you render your own customer content when the response user_group is the control group

  • you render returned Froomle items when the response user_group is the treatment group

  • you use the response user_group and request_id to make the render decision

  • for manual recommendation events, you send user_group for customer/control and omit it for treatment/Froomle

  • you use sendEvent(…​) for manual recommendation-related tracking when auto-tracking is not available

Branch-driven Programmatic Rendering

This is the expected programmatic pattern:

  • send a benchmark-aware request

  • inspect the response user_group

  • if the group is customer/control, render your customer content and ignore returned Froomle items by default

  • if the group is treatment/Froomle, render returned Froomle items, or empty if the response is empty

This is how many existing integrations start. It is valid, but it is not the same as first-class SDK-owned control preservation and tracking. If you render customer content yourself, the SDK has no automatic item identity or DOM ownership unless you also render equivalent metadata into the DOM and use the DOM event layer, or send recommendation events manually.

Recommendation Events From Programmatic UI

In pure JS/TS programmatic integrations, manual recommendation events remain the default because the SDK does not own the rendered placement for you.

Recommendation-related attribution still needs the placement metadata:

  • action_item

  • action_item_type

  • list_name

  • request_id

  • user_group for customer/control only

You have two supported ways to provide that metadata:

  • pass it explicitly in extras

  • let sendEvent(…​) infer it from a rendered source element

Example:

sendEvent(undefined, undefined, 'click_on_recommendation', undefined, {
  source: clickedElement
})

options.source lets the SDK infer recommendation fields from the nearest rendered recommendation or control node:

  • action_item

  • action_item_type

  • list_name

  • request_id

  • user_group when the event belongs to the customer/control branch

Explicit call arguments and explicit extras values still win over inferred values.

If your page instead uses the DOM/script-tag event layer around backend-rendered recommendation nodes, see Advanced Hybrid Pattern for the metadata contract that lets recommendation attribution stay automatic in that supported non-React flow.

React Integrations

React supports standard returned-recommendation rendering and control-aware benchmark rendering. Use standard hooks when you render returned Froomle recommendations outside a customer-vs-Froomle benchmark. Use entry-based rendering when the control branch should render raw CMS/customer items while the SDK still owns recommendation attribution.

React control-aware benchmarking uses render config inside benchmark.

Do not pass the old top-level control / controlItems shape or controlItems[].data. Those shapes are unsupported by the current React API and are ignored.

Standard Froomle React Rendering

For normal Froomle-only rendering, no benchmark control config is needed. Existing useCreateReco(…​), useRecoList(…​).items, and FroomleReco reco={item} usage remains supported.

const { items } = useRecoList({
  list: 'homepage_headline',
  limit: 3,
  list_size: 3,
})

return items.map((item, index) => (
  <FroomleReco reco={item} key={index} asChild>
    <ArticleCard article={item} />
  </FroomleReco>
))

If you add benchmark but no benchmark.controlItems, the hook still follows the resolved branch:

  • treatment/Froomle branch: returned Froomle items are available for rendering

  • customer/control branch: returned Froomle items are ignored for items and entries; provide benchmark.controlItems when React should render and auto-track customer/control cards

useRecoList(…​) With Raw Control Items

Use list-based entries when one component owns the whole list request and the control branch should render existing CMS/customer items.

useRecoList(…​) returns both:

  • items: the branch-aware Froomle-item render list

  • entries: the branch-aware render items for SDK-managed benchmark rendering

const { entries, items } = useRecoList({
  list: 'homepage_headline',
  limit: 3,
  list_size: 3,
  benchmark: {
    mode: 'customer-managed',
    currentGroup: 'customer',
    controlItems: {
      items: arcArticles,
      getId: (article) => article._id,
      getItemType: () => 'article',
    },
    mapRecommendationItem: (reco) => ({
      _id: reco.get('item_id'),
      title: reco.get('title'),
      uri: reco.get('uri'),
      image: reco.get('image'),
    }),
  },
})

return entries.map((entry) => (
  <FroomleReco entry={entry} key={entry.item._id} asChild>
    <ArticleCard article={entry.item} />
  </FroomleReco>
))

Behavior:

  • entries always exists.

  • On the control branch, entries contains exactly the raw items from benchmark.controlItems.items whose getId(…​) result is usable. The SDK does not fill missing control entries with returned Froomle items.

  • On the treatment branch, entries contains returned Froomle recommendations, mapped through benchmark.mapRecommendationItem(…​) when you provide that mapper.

  • If the treatment response is empty, entries is empty.

  • items is empty on customer/control branches, even if the raw response contains returned Froomle items. Inspect response if you need to debug the raw network payload.

  • Raw control items and mapper functions are render-only inputs. They are not sent to the recommendation backend and do not affect request batching.

React list benchmark matrix:

Shape Hook input Result

Froomle-managed Option A

useRecoList({ list, limit, list_size })

Standard list rendering. No explicit benchmark config is needed.

Benchmark request without control items

useRecoList({ …​, benchmark })

entries follows the resolved branch. Treatment/Froomle returns Froomle entries. Customer/control returns no entries because no customer control items were provided; returned Froomle items are ignored for benchmark rendering.

Control-aware control vs Froomle

useRecoList({ …​, benchmark: { …​, controlItems } }) and render entries through FroomleReco entry={entry}

The SDK can keep raw customer items on the control branch, including when Froomle-managed assignment returns user_group: "customer" with an empty items array. It renders Froomle/mapped items on the treatment branch, stamps resolved request metadata, and auto-tracks both branches.

useRecoEntry(…​) For One Raw Control Item

Use useRecoEntry(…​) when one component or card represents one benchmarked slot and the control branch should render one raw customer item.

const entry = useRecoEntry({
  list: 'homepage_headline',
  benchmark: {
    mode: 'customer-managed',
    currentGroup: 'customer',
    controlItem: {
      item: arcArticle,
      getId: (article) => article._id,
      getItemType: () => 'article',
    },
    mapRecommendationItem: (reco) => ({
      _id: reco.get('item_id'),
      title: reco.get('title'),
      uri: reco.get('uri'),
      image: reco.get('image'),
    }),
  },
})

if (!entry) return null

return (
  <FroomleReco entry={entry} asChild>
    <ArticleCard article={entry.item} />
  </FroomleReco>
)

Behavior:

  • On the control branch, the entry item is benchmark.controlItem.item.

  • On the treatment branch, the entry item is the returned Froomle recommendation, mapped when benchmark.mapRecommendationItem(…​) is provided.

  • If the usable entry cannot be resolved, the hook returns null.

useCreateReco(…​) With Benchmark Config

useCreateReco(…​) remains the standard one-hook-per-recommendation API for returned Froomle recommendations. It can receive benchmark request config, but raw customer control items should use useRecoEntry(…​).

React single-slot benchmark matrix:

Shape Hook input Result

Froomle-managed Option A

useCreateReco({ list })

Standard slot-based rendering.

Benchmark request without control item

useCreateReco({ list, benchmark })

Treatment/Froomle can resolve returned Froomle content. Customer/control has no customer item to render, so returned Froomle content is ignored. Use useRecoEntry(…​) with benchmark.controlItem for customer-vs-Froomle single-slot rendering.

Control-aware control vs Froomle

useRecoEntry({ list, benchmark: { …​, controlItem } }) and render the entry through FroomleReco entry={entry}

The SDK can preserve one raw customer item on the control branch, including when Froomle-managed assignment returns user_group: "customer" with an empty items array. It renders Froomle/mapped content on the treatment branch, stamps resolved request metadata, and auto-tracks both branches.

Rendering and Tracking in React

For benchmarked recommendation-item tracking, keep rendering the resolved items through FroomleReco. Use reco={item} for normal Froomle recommendation items and entry={entry} for benchmark entries.

<FroomleReco reco={item} asChild>
  <article className="headline-card">
    <a href={item.get('uri')}>{item.get('title')}</a>
  </article>
</FroomleReco>

Entry-based benchmark rendering:

<FroomleReco entry={entry} asChild>
  <article className="headline-card">
    <a href={entry.item.uri}>{entry.item.title}</a>
  </article>
</FroomleReco>

Use asChild when your card/article root must remain the direct DOM element for layout or CMS CSS.

This matters on both branches because FroomleReco stamps the runtime metadata used for recommendation impression and click tracking:

  • data-froomle-reco

  • data-froomle-id

  • data-froomle-item-type

  • data-froomle-request-id

  • data-froomle-user-group

If you stop rendering through FroomleReco, the SDK can no longer treat that React node as a first-class benchmarked recommendation item automatically.

Tracking Ownership by Benchmark Shape

Integration shape Tracking responsibility

SDK-managed DOM or React control-aware placement

The SDK can auto-track recommendation impressions and clicks on both branches because it owns the resolved placement metadata.

Treatment/Froomle branch with rendered Froomle items

Recommendation auto-tracking works for the rendered Froomle items, as long as they are still rendered through SDK-managed DOM or React primitives.

External control content outside the SDK-managed placement

You still own control rendering and may need explicit manual recommendation events with list_name, request_id, and customer/control user_group.

Customer/control DOM content marked only with data-froomle-reco

This is not enough for automatic control-branch attribution. Add stable data-froomle-id to make it control-aware, or send manual events yourself.

Recommended Implementation Order

For most integrations, the safest rollout order is:

  1. Confirm the business benchmark contract in AB testing and benchmarking.

  2. Start by validating the backend contract and returned user_group.

  3. Move to control-aware SDK ownership only when the control content really lives inside the benchmarked placement.

  4. Recheck impression and click attribution for both branches before calling the benchmark production-ready.

When debugging a live benchmark rollout, inspect window.FroomleFrontendSdkRuntime.diagnostics.benchmark alongside the network tab:

  • defaultConfig tells you which benchmark defaults the SDK currently sees.

  • lastRequestConfig and lastRequestListNames tell you what actually shaped the last benchmarked request.

  • lastResolvedUserGroup, lastResolvedBranch, lastPlacementSurface, lastPlacementVariant, and lastPlacementRenderStrategy tell you what the SDK most recently did with a benchmarked placement.

  • lastUsedReturnedRecommendationContent should stay false under the current strict branch contract; warnings are the quickest indicators of missing/broken control content or skipped React raw control entries.

If lastResolvedBranch is control, lastPlacementVariant is response-driven, lastUsedReturnedRecommendationContent is false, and no impression/click events appear, check whether the visible control cards expose item identity. In declarative DOM/script-tag mode, each automatically tracked control card needs data-froomle-id.

Advanced Integration Notes

Script-tag Whitelist

Only these attributes are read from the SDK <script> tag:

  • data-froomle-env

  • data-froomle-page-visit

  • data-froomle-context-item

  • data-froomle-context-item-type

  • data-froomle-request-domain

  • data-froomle-device-id

  • data-froomle-user-id

  • data-froomle-safe-request

  • data-froomle-channel

Everything else should be set via JS/TS setters.

Event Tracking Behavior

The SDK handles common tracking automatically for DOM-based recommendation rendering. For custom flows and programmatic rendering, use the manual event helpers for supported flows, or sendEvent(…​) when you need the low-level fallback.

If you send events manually, adhere to the standard event contract documented in Tracking events.

Practical rule:

  • If you render recommendations yourself without equivalent runtime metadata, send impression / click_on_recommendation yourself.

  • If you render backend-fetched recommendations into DOM/script-tag markup with data-froomle-reco, data-froomle-id, and data-froomle-request-id, SDK auto-tracking can still work. See Advanced Hybrid Pattern.

  • For React custom rendering outside FroomleReco, treat recommendation attribution as manual unless you intentionally stay inside the normal SDK React rendering flow.

  • Include list_name and request_id for reliable reporting attribution.

action_item_type resolution:

  • detail_pageview: uses context_item_type, defaults to article.

  • impression and click_on_recommendation: use data-froomle-item-type (or data-froomle-action-item-type) when present, otherwise default to article.

  • DOM-filled recommendation nodes automatically get data-froomle-item-type when item_type exists in the recommendation response.

Troubleshooting and Validation

When recommendations do not appear:

  • Verify setEnvironment / data-froomle-env.

  • Verify setPageVisit / data-froomle-page-visit.

  • Check list name (data-froomle-reco or programmatic request list).

  • Check field mappings (uri, images[0], title, or your schema fields).

  • Check network requests to /recommendations/requests.

  • For consent tests, run on http://localhost (cookies do not behave reliably on file://).

  • In webpack dev server setups, keep HMR/live reload disabled when debugging SDK DOM behavior.

Stack-specific checks:

  • Plain HTML: confirm SDK script loads (no CSP/CORS blocking in console).

  • TS/Webpack: confirm SDK init code runs before placeholders are rendered.

  • JSP + TS: confirm the compiled JS bundle is generated and loaded by the JSP page.

  • JSP + TS: for server 503 responses, inspect application server logs and verify JSP/web configuration.

  • Programmatic: if UI renders but events are missing, verify SDK state is initialized (setEnvironment, setPageVisit, consent), and verify helper arguments or sendEvent(…​) extras include the required event fields for your flow.

  • React: verify app is wrapped in FroomleSdkInit and list/filter args passed to useCreateReco are correct.

For full end-to-end sample projects, see frontend-sdk-examples.

Performance

The Froomle frontend SDK is designed to keep browser-side recommendation startup and rendering costs small compared with normal page, CMS, ad, and application work. Froomle continuously guards this with browser performance tests across Chromium, Firefox, and WebKit.

The numbers below are reference lab values, not an SLA and not a guarantee for customer pages. Real-world performance depends on the customer’s page, browser, device, network, CMS, consent layer, ads, and rendering code. Use these values as an SDK-side reference point, then validate the final site with your own Real User Monitoring, Core Web Vitals, and launch checks.

Table 1. Reference lab environment (2026-05-04)

SDK version

0.4.0

Browser harness

Playwright 1.59.1

Runs

5 repeated local runs

Hardware

Model Name: MacBook Pro
Model Identifier: MacBookPro18,3
Chip: Apple M1 Pro
Total Number of Cores: 10 (8 performance and 2 efficiency)
Memory: 32 GB

Method

Median of per-run medians across repeated local Playwright perf runs. Public values use the worst browser median across Chromium, Firefox, and WebKit. Recommendation responses are mocked by the test harness, so backend latency and customer network latency are excluded.

Table 2. Reference browser-side SDK costs
Integration path SDK ready Reco render after response p95 frame gap p95 timer lag Memory cleanup

DOM / script tag startup (blocking)

24 ms

n/a

n/a

6 ms

n/a

DOM / script tag recommendation render (blocking)

35 ms

<1 ms

17 ms

6 ms

pass

DOM / script tag startup (defer)

32 ms

n/a

n/a

6 ms

n/a

DOM / script tag recommendation render (defer)

35 ms

<1 ms

18 ms

6 ms

pass

DOM / script tag startup (async)

34 ms

n/a

n/a

6 ms

n/a

DOM / script tag recommendation render (async)

36 ms

<1 ms

17 ms

8 ms

pass

React SDK startup

17 ms

n/a

n/a

6 ms

n/a

React list recommendation render

16 ms

4 ms

17 ms

6 ms

pass

Programmatic recommendation request

n/a

<1 ms

17 ms

6 ms

pass

Metric interpretation:

  • SDK ready measures time from scenario start until the SDK/runtime is ready for the scenario.

  • Reco render after response measures browser-side recommendation handling after the mocked recommendation response is available. It excludes backend and network latency.

  • p95 frame gap is a main-thread pacing signal. Lower values mean fewer visible long-frame risks during SDK work.

  • p95 timer lag is an event-loop delay signal. Lower values mean less sustained browser-main-thread pressure.

  • Memory cleanup is a cross-browser smoke check that SDK-owned recommendation DOM can be collected after teardown.

  • Deeper CPU and heap counters are kept as internal engineering diagnostics because browser engines expose them differently. The public table uses browser-neutral signals wherever possible.

Implementation guidance:

  • Prefer defer, async, or framework-native initialization over a blocking script tag unless the page has a specific reason to block parsing.

  • Do not block the primary page render on recommendation availability. Render the page shell first and let recommendation modules fill asynchronously.

  • Keep customer card rendering lightweight. The SDK can deliver and insert recommendations quickly, but expensive customer templates, images, ads, or layout work can dominate the final user-visible cost.

  • Keep compatible recommendation request shapes aligned so batching can avoid extra browser and backend round trips.

  • Use fallbacks and timeouts so a slow recommendation response does not create an empty or unstable page area. See Resilience and fallbacks.

  • Use window.FroomleFrontendSdkRuntime.diagnostics during integration QA to verify request split behavior, runtime mode, and recent recommendation or event errors.