From f641856f963241e2c4231ca7e949f02b445165c4 Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 15 Apr 2026 14:18:01 +0200 Subject: [PATCH 1/6] feat: Drupal.rl thin JS API with request batching (#42) Introduces a shared Drupal.rl transport layer so multiple RL consumers on the same page produce ~2 requests instead of one per experiment: - js/rl.js exposes Drupal.rl.decide / turn / reward / flush. Decides flush on the next tick (catching every module that registers in Drupal.behaviors.attach); turns and rewards flush in a 500 ms window and via sendBeacon on visibilitychange / pagehide. - rl.php collapses the legacy turn/turns/reward/decide form handlers into a single action=batch JSON endpoint. ping is preserved for the hook_requirements() health check. - rl_page_attachments() publishes drupalSettings.rl.endpointUrl so consumer modules no longer have to compute and attach the URL themselves. - All four consumer modules (rl_example, rl_example_frontend, rl_menu_link, rl_page_title) migrated to the Drupal.rl API. The broken action=scores call in rl_example_frontend is replaced with Drupal.rl.decide(). - README and docs updated to describe the new JS API. --- README.md | 39 +-- docs/rl_project_desc.html | 30 +- js/rl.js | 261 ++++++++++++++++++ modules/rl_example/js/rl-example-tracking.js | 68 ++--- modules/rl_example/rl_example.libraries.yml | 2 + .../src/Plugin/Block/NewsletterBlock.php | 54 +--- .../js/frontend-ab-testing.js | 165 +++-------- .../rl_example_frontend.libraries.yml | 2 + .../src/Plugin/Block/NewsletterBlock.php | 50 +--- modules/rl_menu_link/README.md | 4 +- modules/rl_menu_link/js/menu-tracking.js | 31 +-- .../rl_menu_link/rl_menu_link.libraries.yml | 1 + modules/rl_menu_link/rl_menu_link.module | 8 +- modules/rl_page_title/README.md | 5 +- modules/rl_page_title/js/title-tracking.js | 32 +-- .../rl_page_title/rl_page_title.libraries.yml | 1 + modules/rl_page_title/rl_page_title.module | 6 +- rl.libraries.yml | 8 + rl.module | 7 + rl.php | 200 ++++++++++---- 20 files changed, 575 insertions(+), 399 deletions(-) create mode 100644 js/rl.js diff --git a/README.md b/README.md index 8fbce54..ebda71e 100644 --- a/README.md +++ b/README.md @@ -213,27 +213,34 @@ $cache_manager = \Drupal::service('rl.cache_manager'); $cache_manager->overridePageCacheIfShorter(60); // 60 seconds ``` -## HTTP Endpoints +## JavaScript API -### rl.php - Direct Endpoint (Recommended) -**Use the direct rl.php endpoint for optimal performance:** +Attach the `rl/api` library to make `Drupal.rl` available. It is a thin transport proxy that coalesces every RL call on the page into a single batched POST to `rl.php`, so N experiments on the same page produce ~2 requests regardless of how many modules are tracking. ```javascript -// Record turns (trials) - when content is viewed -const formData = new FormData(); -formData.append('action', 'turns'); -formData.append('experiment_id', 'abc123'); -formData.append('arm_ids', '1,2,3'); -navigator.sendBeacon('/modules/contrib/rl/rl.php', formData); - -// Record reward (success) - when user clicks/converts -const rewardData = new FormData(); -rewardData.append('action', 'rewards'); -rewardData.append('experiment_id', 'abc123'); -rewardData.append('arm_id', '1'); -navigator.sendBeacon('/modules/contrib/rl/rl.php', rewardData); +// Ask for a Thompson Sampling decision - resolves to the winning arm id. +Drupal.rl.decide('hero_cta', ['v0', 'v1', 'v2']).then(function (armId) { + renderVariant(armId); +}); + +// Record an impression. +Drupal.rl.turn('hero_cta', 'v0'); + +// Record a conversion. +Drupal.rl.reward('hero_cta', 'v0'); ``` +Decides flush on the next tick so every module that registers during `Drupal.behaviors.attach` shares one request. Turns and rewards flush in a 500 ms window, and buffered events are flushed via `navigator.sendBeacon` on `visibilitychange` / `pagehide` so they survive navigation. + +### rl.php endpoint + +`rl.php` is the low-level HTTP endpoint `Drupal.rl` talks to. It accepts two actions: + +- `action=ping` - liveness check used by `hook_requirements()`. +- `action=batch` - JSON body used by `Drupal.rl`. See `js/rl.js` for the payload shape. + +Direct consumption of `action=batch` without going through `Drupal.rl` is not recommended. + ## Cache Management RL provides optional cache management for web components: diff --git a/docs/rl_project_desc.html b/docs/rl_project_desc.html index 39b6804..05de4ff 100644 --- a/docs/rl_project_desc.html +++ b/docs/rl_project_desc.html @@ -62,7 +62,7 @@

Installation

Verify rl.php Access

The RL module includes a .htaccess file that allows direct access to rl.php (following the same pattern as Drupal 11's contrib statistics module). Test that it's working:

-
curl -X POST -d "action=turns&experiment_id=test&arm_ids=1" http://example.com/modules/contrib/rl/rl.php
+
curl -X POST -d "action=ping" http://example.com/modules/contrib/rl/rl.php

If the test fails: