Skip to content

v1.0.2 — JS Beacon Safety Fixes

Choose a tag to compare

@schoedel-learn schoedel-learn released this 12 Apr 21:33
· 3 commits to main since this release

Bug Fix: JavaScript Beacon Interfering with xAPI Module Loading

Update immediately if you installed v1.0.0 or v1.0.1. These bugs could cause Rise content to appear stuck, generate an infinite network loop, and produce incorrect monitoring data.


Four Bugs Fixed

Bug 1 — Infinite Self-Monitoring Loop (Critical)

Pattern: /xapi/i

This pattern matched the beacon's own REST endpoint URL (/xapi-monitor/v1/beacon). Every time the beacon POSTed monitoring data to itself, the XHR interceptor detected it as an xAPI delivery, fired another beacon about it, which was again intercepted, and so on — an infinite loop running on every lesson page.

Fix: The beacon's own URL is now in a hard EXCLUSIONS list checked before any pattern matching. It can never be intercepted.


Bug 2 — False Positive Intercepts on Rise Assets (Critical)

Pattern: /statements/i

Too broad. Matched any URL containing the word "statements" — including:

  • Rise JavaScript asset filenames (e.g. story_content/statements.js)
  • Google Font loading URLs with "statements" in query params
  • Unrelated WordPress REST routes

The beacon was intercepting and logging these asset loads as if they were xAPI statement deliveries, polluting the monitoring log with junk data.

Fix: Removed. Replaced with /\/statements\// (only matches the path segment used by real xAPI LRS endpoints) and /[?&]ucTinCan/i (the Tin Canny query-string form).


Bug 3 — XHR Handler Interference Breaking Rise Statement Delivery (Critical)

The beacon replaced onreadystatechange on each XHR object at send() time, wrapping whatever handler existed at that moment.

The problem: Rise sets its own onreadystatechange handler after calling send() — a completely valid JavaScript pattern. Our wrapper only captured the handler present at send() time, silently dropping anything assigned later. This meant Rise's completion callback (the code that determines whether to show the next slide or mark the course complete) could fail to fire after a statement was sent.

Fix: Replaced onreadystatechange replacement with addEventListener('load') and addEventListener('readystatechange'). Event listeners stack and never conflict with each other regardless of assignment order. Added a beaconSent boolean guard to prevent duplicate reports from both events firing on the same request.


Bug 4 — MutationObserver Blocking Rise Slide Rendering (Moderate)

hookIframe() accessed contentDocument synchronously inside the MutationObserver callback, before the iframe had finished loading.

Accessing a not-yet-loaded iframe's contentDocument blocks the browser's DOM mutation queue — the same queue Rise uses to swap slide content and advance through the module. Depending on timing this caused slides to appear stuck or fail to advance to the next screen.

Additionally, the MutationObserver callback did not check node.nodeType before accessing node.tagName, throwing silent errors on text and comment nodes.

Fix: All iframe hooks are now deferred with setTimeout(fn, 0) after the iframe's load event, giving Rise's own load handlers full priority and keeping the mutation queue unblocked. Added nodeType !== 1 guard to skip non-element nodes.