From 800688ca70cc75b8db6e80275bbf4723ebeedfa3 Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Mon, 20 Apr 2026 09:21:29 +0200 Subject: [PATCH 1/4] fix(rl.php): return 422 + errors on fully-rejected batches Why: rl.php used to swallow every rejected decide/turn/reward with a `continue` and reply 200 `{"ok":true,"decisions":{}}`. The Drupal.rl client then falls back to `armIds[0]` silently, so a site-wide misconfiguration (e.g. a new entity-hook that never got its implementations cache rebuilt, leaving experiments unregistered despite the container being stamped with a valid-looking eid) is indistinguishable from a healthy no-op request. The operator has nothing to grep for in devtools or webserver logs; variants just appear to never rotate. handle_batch_request now: - returns a structured array with `decisions`, `errors`, `requested`, `succeeded` - tags each rejected entry with a machine-readable reason (unknown_experiment, invalid_id, missing_arms, invalid_arm_id, scoring_failed, manager_unavailable, malformed_entry) - lets the caller pick the status code: 422 when a non-empty batch produced zero useful work, 200 for partial or full success Partial failures stay 200 on purpose. A page rendering three healthy containers plus one stale one should still get decisions for the three, not reject the whole batch because one container is wrong mid-deploy. The `errors` array surfaces the stale one to the client. rl.js flushes the `errors` array to `console.warn` and forwards to an optional `Drupal.rl.onErrors` listener so advanced consumers can wire an alerting path without patching rl/api. --- js/rl.js | 33 +++++++++++++++++- rl.php | 104 ++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 112 insertions(+), 25 deletions(-) diff --git a/js/rl.js b/js/rl.js index 393043e..f753a6c 100644 --- a/js/rl.js +++ b/js/rl.js @@ -155,13 +155,23 @@ credentials: 'same-origin', keepalive: true, }).then(function (response) { - if (!response.ok) { + // rl.php returns 422 when every entry in a non-empty batch was + // rejected (unknown experiment, invalid ids, manager + // unavailable). Read the JSON body anyway so the errors array + // reaches the console — without this, a site-wide mistake like + // "registry cache missed the new hook so no experiment rows + // exist" looks identical to a healthy empty batch and the + // operator has nothing to grep for in devtools. Other non-2xx + // statuses (4xx malformed, 5xx bootstrap failure) have no + // useful body, so fall back. + if (!response.ok && response.status !== 422) { fallbackDecides(snapshot); return null; } return response.json(); }).then(function (json) { if (json) { + reportErrors(json.errors); resolveDecides(snapshot, json.decisions || {}); } }).catch(function () { @@ -169,6 +179,27 @@ }); } + // Surface rl.php's per-entry errors on the console so mis-registered + // experiments become visible in devtools instead of silently falling + // back to armIds[0]. Runtimes that need richer handling can stub + // window.Drupal.rl.onErrors; by default we only warn. + function reportErrors(errors) { + if (!Array.isArray(errors) || !errors.length) { + return; + } + if (typeof Drupal.rl.onErrors === 'function') { + try { Drupal.rl.onErrors(errors); } catch (e) { /* never let a listener poison the next flush */ } + } + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + errors.forEach(function (err) { + if (!err || typeof err !== 'object') { return; } + console.warn( + '[rl] ' + (err.kind || 'entry') + ' for ' + (err.id || '(no id)') + ' rejected: ' + (err.reason || 'unknown') + ); + }); + } + } + function flushBeacon() { if (!hasPending()) { return; diff --git a/rl.php b/rl.php index 199bb53..a5f4ea4 100644 --- a/rl.php +++ b/rl.php @@ -105,11 +105,26 @@ $manager = $container->has('rl.experiment_manager') ? $container->get('rl.experiment_manager') : NULL; if ($action === 'batch') { - $decisions = handle_batch_request($payload, $registry, $storage, $manager); - http_response_code(200); + $result = handle_batch_request($payload, $registry, $storage, $manager); + // Status code reflects whether the batch produced any real work. + // A non-empty batch whose every entry was rejected (all unknown + // experiments, all invalid arm ids, etc.) comes back as 422 + // Unprocessable Entity so the client can log + alert. Empty or + // partially successful batches stay 200 — partial success is the + // normal case when one container is stale mid-deploy and other + // containers on the page are fine. + $status = ($result['requested'] > 0 && $result['succeeded'] === 0) ? 422 : 200; + http_response_code($status); header('Content-Type: application/json'); header('Cache-Control: no-store, private, max-age=0'); - echo json_encode(['ok' => TRUE, 'decisions' => $decisions]); + $body = [ + 'ok' => $status === 200, + 'decisions' => $result['decisions'], + ]; + if (!empty($result['errors'])) { + $body['errors'] = $result['errors']; + } + echo json_encode($body); exit; } @@ -179,10 +194,13 @@ * {"decisions": {"": {"armId": ""}, ...}} * @endcode * - * Unknown or malformed entries are silently skipped so one bad event - * does not poison the rest of the batch. Callers that receive no - * decision for a given experiment should fall back to the first arm - * they passed in. + * Rejected entries are reported in the returned `errors` list with a + * machine-readable reason ("unknown_experiment", "invalid_arm_id", + * "missing_arms", "invalid_id", "scoring_failed", "malformed_entry"). + * The caller decides the HTTP status: a batch where every entry was + * rejected is 422; partial failures stay 200 with an `errors` array + * so a stale container on one page does not poison decides for + * healthy containers sharing the same request. * * @param array $payload * The decoded JSON body. @@ -192,32 +210,53 @@ * The experiment data storage used to persist turns and rewards. * @param \Drupal\rl\Service\ExperimentManagerInterface|null $manager * The experiment manager used to compute Thompson Sampling scores. - * May be NULL if the service is unavailable, in which case no - * decisions are produced. + * May be NULL if the service is unavailable, in which case every + * decide entry is reported as a "manager_unavailable" error. * - * @return object - * A stdClass keyed by experiment id. Empty object if no decides were - * requested or resolved. Uses stdClass so json_encode emits `{}` - * instead of `[]` when the map is empty. + * @return array{ + * decisions: \stdClass, + * errors: array, + * requested: int, + * succeeded: int, + * } + * `decisions` is a stdClass keyed by experiment id so json_encode + * emits `{}` for an empty map. `requested` counts every non-empty + * entry seen across decides/turns/rewards; `succeeded` counts the + * ones that actually did useful work. The caller uses the two + * counters to pick a status code. */ -function handle_batch_request(array $payload, $registry, $storage, $manager = NULL): \stdClass { +function handle_batch_request(array $payload, $registry, $storage, $manager = NULL): array { $id_pattern = '/^[a-zA-Z0-9_-]+$/'; $decisions = new \stdClass(); + $errors = []; + $requested = 0; + $succeeded = 0; - if ($manager !== NULL && isset($payload['decides']) && is_array($payload['decides'])) { + if (isset($payload['decides']) && is_array($payload['decides'])) { foreach ($payload['decides'] as $decide) { if (!is_array($decide)) { + $requested++; + $errors[] = ['kind' => 'decide', 'id' => '', 'reason' => 'malformed_entry']; continue; } $eid = isset($decide['id']) ? (string) $decide['id'] : ''; if ($eid === '' || !preg_match($id_pattern, $eid)) { + $requested++; + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'invalid_id']; + continue; + } + $requested++; + if ($manager === NULL) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'manager_unavailable']; continue; } if (!$registry->isRegistered($eid)) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'unknown_experiment']; continue; } $arms = $decide['arms'] ?? []; if (!is_array($arms) || count($arms) < 2) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'missing_arms']; continue; } $arm_ids = []; @@ -231,61 +270,78 @@ function handle_batch_request(array $payload, $registry, $storage, $manager = NU $arm_ids[] = $arm_id; } if (!$valid) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'invalid_arm_id']; continue; } try { $scores = $manager->getThompsonScores($eid, NULL, $arm_ids); } catch (\Throwable $e) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'scoring_failed']; continue; } if (!is_array($scores) || !$scores) { + $errors[] = ['kind' => 'decide', 'id' => $eid, 'reason' => 'scoring_failed']; continue; } arsort($scores); $decisions->{$eid} = ['armId' => (string) key($scores)]; + $succeeded++; } } if (isset($payload['turns']) && is_array($payload['turns'])) { foreach ($payload['turns'] as $turn) { if (!is_array($turn)) { + $requested++; + $errors[] = ['kind' => 'turn', 'id' => '', 'reason' => 'malformed_entry']; continue; } $eid = isset($turn['id']) ? (string) $turn['id'] : ''; $aid = isset($turn['arm']) ? (string) $turn['arm'] : ''; - if ($eid === '' || $aid === '') { - continue; - } - if (!preg_match($id_pattern, $eid) || !preg_match($id_pattern, $aid)) { + if ($eid === '' || $aid === '' || !preg_match($id_pattern, $eid) || !preg_match($id_pattern, $aid)) { + $requested++; + $errors[] = ['kind' => 'turn', 'id' => $eid, 'reason' => 'invalid_id']; continue; } + $requested++; if (!$registry->isRegistered($eid)) { + $errors[] = ['kind' => 'turn', 'id' => $eid, 'reason' => 'unknown_experiment']; continue; } $storage->recordTurn($eid, $aid); + $succeeded++; } } if (isset($payload['rewards']) && is_array($payload['rewards'])) { foreach ($payload['rewards'] as $reward) { if (!is_array($reward)) { + $requested++; + $errors[] = ['kind' => 'reward', 'id' => '', 'reason' => 'malformed_entry']; continue; } $eid = isset($reward['id']) ? (string) $reward['id'] : ''; $aid = isset($reward['arm']) ? (string) $reward['arm'] : ''; - if ($eid === '' || $aid === '') { - continue; - } - if (!preg_match($id_pattern, $eid) || !preg_match($id_pattern, $aid)) { + if ($eid === '' || $aid === '' || !preg_match($id_pattern, $eid) || !preg_match($id_pattern, $aid)) { + $requested++; + $errors[] = ['kind' => 'reward', 'id' => $eid, 'reason' => 'invalid_id']; continue; } + $requested++; if (!$registry->isRegistered($eid)) { + $errors[] = ['kind' => 'reward', 'id' => $eid, 'reason' => 'unknown_experiment']; continue; } $storage->recordReward($eid, $aid); + $succeeded++; } } - return $decisions; + return [ + 'decisions' => $decisions, + 'errors' => $errors, + 'requested' => $requested, + 'succeeded' => $succeeded, + ]; } From ea5751dc5c73089fe8d48ca2b3f75c55fc12c40a Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 29 Apr 2026 14:18:29 +0200 Subject: [PATCH 2/4] Improve README.md and project description SEO - Add branded DXPR header with links to dxpr.com product pages - Improve H1 title with descriptive keyword-rich suffix - Add Related DXPR Modules section for cross-linking --- README.md | 14 +++++++++++++- docs/rl_project_desc.html | 27 ++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2843545..09beac4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# Reinforcement Learning (RL) +> Part of [DXPR CMS](https://dxpr.com/c/marketing-cms): The AI-Powered Drupal CMS +> +> [Documentation](https://dxpr.com/docs) | [Try Free](https://dxpr.com/try) | [dxpr.com](https://dxpr.com) + +# Reinforcement Learning (RL): Adaptive A/B Testing for Drupal with Thompson Sampling Multi-armed bandit experiments in Drupal using Thompson Sampling algorithm for efficient A/B testing that minimizes lost conversions. @@ -539,3 +543,11 @@ docker compose --profile lint run --rm drupal-check - [Thompson Sampling Paper](https://www.jstor.org/stable/2332286) - Original research - [Finite-time Analysis](https://homes.di.unimi.it/~cesa-bianchi/Pubblicazioni/ml-02.pdf) - Mathematical foundations + +--- + +## Related DXPR Modules + +- [RL Sorting](https://www.drupal.org/project/rl_sorting): Intelligent content ordering for Drupal Views using reinforcement learning +- [Analyze](https://www.drupal.org/project/analyze): Content analysis and quality scoring for Drupal +- [AI Content Strategy](https://www.drupal.org/project/ai_content_strategy): AI-driven content strategy recommendations for Drupal diff --git a/docs/rl_project_desc.html b/docs/rl_project_desc.html index e96207e..7662354 100644 --- a/docs/rl_project_desc.html +++ b/docs/rl_project_desc.html @@ -1,3 +1,5 @@ +

Part of the DXPR ecosystem: the AI-powered no-code platform for Drupal. Try free | Documentation

+

This module is included in DXPR CMS.

The Reinforcement Learning (RL) module implements A/B testing in the most efficient and effective way possible, minizing lost conversions using machine learning.
@@ -5,7 +7,7 @@

Thompson Sampling is a learning-while-doing method. Each time a visitor lands on your site the algorithm "rolls the dice" based on what it has learned so far. Variants that have performed well roll larger numbers, so they are shown more often, while weak copies still get a small chance to prove themselves. This simple trick means the system can discover winners very quickly without stopping normal traffic.

-

Traditional A/B tests run for a fixed horizon—say two weeks—during which half your visitors keep seeing the weaker version. Thompson Sampling avoids this waste. As soon as the algorithm has even a little evidence it quietly shifts most traffic to the better variant, saving conversions and shortening the wait for useful insights.

+

Traditional A/B tests run for a fixed horizon (say two weeks) during which half your visitors keep seeing the weaker version. Thompson Sampling avoids this waste. As soon as the algorithm has even a little evidence it quietly shifts most traffic to the better variant, saving conversions and shortening the wait for useful insights.

For full details of what goes on behind the curtains check the source code: ThompsonCalculator.php. @@ -50,7 +52,7 @@

Thompson Sampling

Prefer a turnkey demo site?

-

Spin up DXPR CMS—Drupal pre-configured with DXPR Builder, DXPR Theme, RL (Reinforcement Learning) module, and security best practices.

+

Spin up DXPR CMS, Drupal pre-configured with DXPR Builder, DXPR Theme, RL (Reinforcement Learning) module, and security best practices.

Get DXPR CMS »

@@ -193,13 +195,13 @@

Does RL store my experiment's variants?

Different modules keep the live variant list in different places:

    -
  • ai_sorting — the content returned by a View
  • -
  • rl_page_title — fields on a content entity
  • -
  • rl_menu_link — labels in a menu link
  • -
  • DXPR Builder — slots inside a block component
  • +
  • ai_sorting: the content returned by a View
  • +
  • rl_page_title: fields on a content entity
  • +
  • rl_menu_link: labels in a menu link
  • +
  • DXPR Builder: slots inside a block component
-

On each call, your module passes its current list — getThompsonScores($id, NULL, $arms) in PHP or Drupal.rl.decide(id, arms) in JS — and RL matches it against the stored stats to pick a winner. A newly added variant is automatically in play on the next render, and a removed one stops appearing. No second save step can fall out of sync with your module's UI.

+

On each call, your module passes its current list (getThompsonScores($id, NULL, $arms) in PHP or Drupal.rl.decide(id, arms) in JS) and RL matches it against the stored stats to pick a winner. A newly added variant is automatically in play on the next render, and a removed one stops appearing. No second save step can fall out of sync with your module's UI.

When do I pick a winner and end an experiment?

@@ -208,12 +210,19 @@

When do I pick a winner and end an experiment?

Two patterns, depending on what you are testing:

    -
  • Converging tests — a better page title, a clearer checkout button, a stronger hero image. Once the report shows a confident winner, lock it in and move on to the next test.
  • -
  • Evergreen experiments — blog post lists where reader interest drifts week to week, banner ads that fade as returning visitors tune them out, seasonal calls to action. Leave them running. Thompson Sampling will follow the winner as it shifts.
  • +
  • Converging tests: a better page title, a clearer checkout button, a stronger hero image. Once the report shows a confident winner, lock it in and move on to the next test.
  • +
  • Evergreen experiments: blog post lists where reader interest drifts week to week, banner ads that fade as returning visitors tune them out, seasonal calls to action. Leave them running. Thompson Sampling will follow the winner as it shifts.

In both cases the loser of a pair just stops receiving traffic on its own, so there is no urgency to declare a winner by hand. If you are used to fixed-horizon A/B tools, this is the biggest mental shift: there is no "test complete" flag to chase.

+

Related DXPR Modules

+
    +
  • RL Sorting: Intelligent content ordering for Drupal Views using reinforcement learning
  • +
  • Analyze: Content analysis and quality scoring for Drupal
  • +
  • AI Content Strategy: AI-driven content strategy recommendations for Drupal
  • +
+

Resources

  • Free Micro-Course on Thompson Sampling
  • From 0d39cd00e4cbfdd2abc6b726bcc27405b44ae98c Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 29 Apr 2026 17:27:50 +0200 Subject: [PATCH 3/4] Revise SEO approach: full optimization on README, community-friendly desc.html - README: GEO-optimized unique header with entity clarity and varied anchor text - README: Add Pricing and Getting Started links - README: Related Modules now includes non-DXPR community modules - desc.html: Remove commercial branded header (community space) - desc.html: Related Modules now includes non-DXPR community modules --- README.md | 18 +++++++++++------- docs/rl_project_desc.html | 14 +++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 09beac4..d71fc94 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -> Part of [DXPR CMS](https://dxpr.com/c/marketing-cms): The AI-Powered Drupal CMS +> **Reinforcement Learning (RL)** brings Thompson Sampling multi-armed bandit A/B testing to Drupal, automatically optimizing content variants based on real user engagement without third-party services. Built by [DXPR](https://dxpr.com). > -> [Documentation](https://dxpr.com/docs) | [Try Free](https://dxpr.com/try) | [dxpr.com](https://dxpr.com) +> [Getting Started](https://dxpr.com/c/getting-started) | +> [Pricing](https://dxpr.com/pricing) | +> [Try Free Demo](https://dxpr.com/try) -# Reinforcement Learning (RL): Adaptive A/B Testing for Drupal with Thompson Sampling +# Reinforcement Learning (RL) - Adaptive A/B Testing for Drupal with Thompson Sampling Multi-armed bandit experiments in Drupal using Thompson Sampling algorithm for efficient A/B testing that minimizes lost conversions. @@ -546,8 +548,10 @@ docker compose --profile lint run --rm drupal-check --- -## Related DXPR Modules +## Related Modules -- [RL Sorting](https://www.drupal.org/project/rl_sorting): Intelligent content ordering for Drupal Views using reinforcement learning -- [Analyze](https://www.drupal.org/project/analyze): Content analysis and quality scoring for Drupal -- [AI Content Strategy](https://www.drupal.org/project/ai_content_strategy): AI-driven content strategy recommendations for Drupal +- [RL Sorting](https://www.drupal.org/project/rl_sorting) - Intelligent content ordering for Drupal Views using reinforcement learning +- [Analyze](https://www.drupal.org/project/analyze) - Content analysis and quality scoring for Drupal +- [AI Content Strategy](https://www.drupal.org/project/ai_content_strategy) - AI-driven content strategy recommendations for Drupal +- [Google Tag](https://www.drupal.org/project/google_tag) - Google Tag Manager integration for Drupal +- [ECA](https://www.drupal.org/project/eca) - Event-Condition-Action framework for Drupal automation diff --git a/docs/rl_project_desc.html b/docs/rl_project_desc.html index 7662354..efb5e9f 100644 --- a/docs/rl_project_desc.html +++ b/docs/rl_project_desc.html @@ -1,8 +1,6 @@ -

    Part of the DXPR ecosystem: the AI-powered no-code platform for Drupal. Try free | Documentation

    -

    This module is included in DXPR CMS.

    -
    The Reinforcement Learning (RL) module implements A/B testing in the most efficient and effective way possible, minizing lost conversions using machine learning.
    +
    The Reinforcement Learning (RL) module implements A/B testing in the most efficient and effective way possible, minimizing lost conversions using machine learning.

    Thompson Sampling is a learning-while-doing method. Each time a visitor lands on your site the algorithm "rolls the dice" based on what it has learned so far. Variants that have performed well roll larger numbers, so they are shown more often, while weak copies still get a small chance to prove themselves. This simple trick means the system can discover winners very quickly without stopping normal traffic.

    @@ -216,11 +214,13 @@

    When do I pick a winner and end an experiment?

    In both cases the loser of a pair just stops receiving traffic on its own, so there is no urgency to declare a winner by hand. If you are used to fixed-horizon A/B tools, this is the biggest mental shift: there is no "test complete" flag to chase.

    -

    Related DXPR Modules

    +

    Related Modules

      -
    • RL Sorting: Intelligent content ordering for Drupal Views using reinforcement learning
    • -
    • Analyze: Content analysis and quality scoring for Drupal
    • -
    • AI Content Strategy: AI-driven content strategy recommendations for Drupal
    • +
    • RL Sorting - Intelligent content ordering for Drupal Views using reinforcement learning
    • +
    • Analyze - Content analysis and quality scoring for Drupal
    • +
    • AI Content Strategy - AI-driven content strategy recommendations for Drupal
    • +
    • Google Tag - Google Tag Manager integration for Drupal
    • +
    • ECA - Event-Condition-Action framework for Drupal automation

    Resources

    From 8cd92459aab1a61ce6468d800371e5d92209187f Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 29 Apr 2026 17:45:23 +0200 Subject: [PATCH 4/4] Improve related modules based on actual integrations - Add bundled submodules RL Page Title and RL Menu Link to related modules - Replace AI Sorting reference with RL Sorting (correct project name) - Remove Google Tag and ECA which have no direct code integration - Remove duplicate Related Modules section mid-README - Add contextual dxpr.com/c/marketing-cms link in desc.html - Add RL Ecosystem section listing the full module family --- README.md | 11 +++-------- docs/rl_project_desc.html | 12 +++++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d71fc94..a6ab00a 100644 --- a/README.md +++ b/README.md @@ -430,11 +430,6 @@ RL provides optional cache management for web components: - Blocks displaying A/B tested content - Components needing frequent RL score updates -## Related Modules - -- [AI Sorting](https://www.drupal.org/project/ai_sorting) - Intelligent content - ordering for Drupal Views - ## Technical Implementation Full algorithm details available in source code: @@ -550,8 +545,8 @@ docker compose --profile lint run --rm drupal-check ## Related Modules -- [RL Sorting](https://www.drupal.org/project/rl_sorting) - Intelligent content ordering for Drupal Views using reinforcement learning +- [RL Sorting](https://www.drupal.org/project/rl_sorting) - Views sort plugin that uses RL Thompson Sampling to order content by real engagement +- **RL Page Title** (bundled submodule) - A/B test page titles on any path, including nodes, Views displays, and custom controllers +- **RL Menu Link** (bundled submodule) - A/B test menu link labels for menu_link_content entities and YAML-defined links - [Analyze](https://www.drupal.org/project/analyze) - Content analysis and quality scoring for Drupal - [AI Content Strategy](https://www.drupal.org/project/ai_content_strategy) - AI-driven content strategy recommendations for Drupal -- [Google Tag](https://www.drupal.org/project/google_tag) - Google Tag Manager integration for Drupal -- [ECA](https://www.drupal.org/project/eca) - Event-Condition-Action framework for Drupal automation diff --git a/docs/rl_project_desc.html b/docs/rl_project_desc.html index efb5e9f..231bc84 100644 --- a/docs/rl_project_desc.html +++ b/docs/rl_project_desc.html @@ -11,12 +11,16 @@ ThompsonCalculator.php.

    -

    RL Modules

    +

    RL Ecosystem

      -
    • AI Sorting - Intelligent content ordering/switching for Drupal Views
    • +
    • RL Sorting - Views sort plugin that uses RL Thompson Sampling to order content by real engagement
    • +
    • RL Page Title (bundled) - A/B test page titles on any path: nodes, Views displays, custom controllers
    • +
    • RL Menu Link (bundled) - A/B test menu link labels for menu_link_content entities and YAML-defined links
    +

    RL is part of the DXPR marketing CMS stack, where it works alongside DXPR Builder and DXPR Theme to optimize content through automated experimentation.

    +

    Features

      @@ -216,11 +220,9 @@

      When do I pick a winner and end an experiment?

      Related Modules

        -
      • RL Sorting - Intelligent content ordering for Drupal Views using reinforcement learning
      • +
      • RL Sorting - Views sort plugin that uses RL Thompson Sampling to order content by real engagement
      • Analyze - Content analysis and quality scoring for Drupal
      • AI Content Strategy - AI-driven content strategy recommendations for Drupal
      • -
      • Google Tag - Google Tag Manager integration for Drupal
      • -
      • ECA - Event-Condition-Action framework for Drupal automation

      Resources