Open edX Proctoring JavaScript Architecture: JS 1.0 vs JS 2.0
Date: November 5, 2024
Author: Investigation of edx-proctoring and edx-platform integration
Version: 1.0
Executive Summary
Open edX supports two different approaches for loading proctoring provider JavaScript code, commonly referred to as "JS 1.0" and "JS 2.0". Understanding the difference is critical for proper deployment configuration, especially when using containerized builds.
Table of Contents
- Overview
- JS 1.0: Legacy LMS Template Approach
- JS 2.0: Modern MFE API Approach
- Build-Time vs Runtime Configuration
- Key Differences Summary
Overview
The Open edX platform needs to load proctoring provider-specific JavaScript to communicate with desktop proctoring applications. There are two approaches:
- JS 1.0: JavaScript bundles embedded in Django templates (legacy approach)
- JS 2.0: JavaScript bundle URLs returned via REST API (modern MFE approach)
Both approaches can coexist in the same deployment and serve different frontend applications.
JS 1.0: Legacy LMS Template Approach
Architecture
In the traditional LMS courseware interface, proctoring JavaScript is loaded directly into Django-rendered HTML templates.
How It Works
-
Backend generates JavaScript bundle URL:
# edx_proctoring/backends/rest.py def get_javascript(self): package = getattr(self, 'npm_module', self.__class__.__module__.split('.', maxsplit=1)[0]) bundle_chunks = get_files(package, config="WORKERS") js_url = bundle_chunks[0]["url"] if not urlparse(js_url).scheme: if hasattr(settings, 'LMS_ROOT_URL'): js_url = settings.LMS_ROOT_URL + js_url return js_url -
Template context includes bundle URL:
-
Template embeds the URL:
<!-- edx_proctoring/templates/proctored_exam/ready_to_start.html --> <script type="text/javascript"> var edx = edx || {}; edx.courseware = edx.courseware || {}; edx.courseware.proctored_exam = edx.courseware.proctored_exam || {}; edx.courseware.proctored_exam.configuredWorkerURL = "{{ backend_js_bundle }}"; </script> -
JavaScript creates Web Worker:
// edx_proctoring/static/proctoring/js/exam_action_handler.js function createWorker(url) { var blob = new Blob(["importScripts('" + url + "');"], {type: 'application/javascript'}); var blobUrl = window.URL.createObjectURL(blob); return new Worker(blobUrl); } var proctoringBackendWorker = createWorker( edx.courseware.proctored_exam.configuredWorkerURL );
Configuration Requirements
Build-Time:
- Webpack must build proctoring worker bundles
- webpack-worker-stats.json must exist for django-webpack-loader
- workers.json must be generated before webpack runs
Runtime:
- Django settings must include proctoring backend configuration
- Templates must have access to backend_js_bundle variable
Used By
- Traditional LMS courseware interface
- Django-rendered exam pages
- Legacy proctored exam workflows
JS 2.0: Modern MFE API Approach
Architecture
The Learning Microfrontend (MFE) fetches exam attempt data via API, which includes the JavaScript worker bundle URL.
How It Works
-
MFE requests exam attempt data:
-
API response includes worker URL:
-
MFE extracts URL from response:
-
MFE creates worker dynamically:
Configuration Requirements
Build-Time: - Same as JS 1.0 (webpack must build worker bundles) - Worker bundles must be available at known URLs
Runtime:
- API endpoints must be accessible
- Backend must return desktop_application_js_url in responses
- No template context required
Used By
- Learning MFE (frontend-app-learning)
- frontend-lib-special-exams library
- Modern exam workflows with API-driven UIs
Build-Time vs Runtime Configuration
What Happens at Build Time
-
Django app initialization (when
manage.pyruns):# edx_proctoring/apps.py def ready(self): # Loads all configured backends self.backends = {} for extension in ExtensionManager(namespace='openedx.proctoring'): self.backends[name] = extension.plugin(**options) # Generates workers.json make_worker_config( list(self.backends.values()), out=os.path.join(settings.ENV_ROOT, 'workers.json') ) -
workers.json generation:
# edx_proctoring/apps.py def make_worker_config(backends, out='/tmp/workers.json'): if not getattr(settings, 'NODE_MODULES_ROOT', None): return False config = {} for backend in backends: package = backend.npm_module # e.g., 'edx-proctoring-proctortrack' package_file = os.path.join( settings.NODE_MODULES_ROOT, package, 'package.json' ) with open(package_file, 'r') as f: package_json = json.load(f) main_file = package_json['main'] config[package] = [ 'babel-polyfill', os.path.join(settings.NODE_MODULES_ROOT, package, main_file) ] with open(out, 'wb+') as outfp: outfp.write(json.dumps(config).encode('utf-8')) -
Webpack consumes workers.json:
// edx-platform/webpack.common.config.js var workerConfig = function() { try { return { webworker: { entry: require('../workers.json'), // <-- reads the file output: { filename: '[name].js', path: path.resolve(__dirname, 'common/static/bundles') }, plugins: [ new BundleTracker({ filename: 'webpack-worker-stats.json' }), new webpack.DefinePlugin({ 'process.env.JS_ENV_EXTRA_CONFIG': JSON.parse(process.env.JS_ENV_EXTRA_CONFIG) }) ] } }; } catch (err) { return {}; // Silently returns empty if workers.json missing! } }; -
Webpack builds bundles and creates
webpack-worker-stats.json
What Happens at Runtime
-
Backend looks up bundle URL:
-
URL is served via:
- JS 1.0: Template context variable
{{ backend_js_bundle }} -
JS 2.0: API response field
desktop_application_js_url -
Frontend loads the worker from the URL
Key Differences Summary
| Aspect | JS 1.0 (Legacy) | JS 2.0 (MFE) |
|---|---|---|
| Frontend | Django templates | React MFE |
| Data Flow | Template context | REST API |
| Worker URL Location | Template variable | API response field |
| JS Variable | edx.courseware.proctored_exam.configuredWorkerURL |
desktop_application_js_url |
| Template Files | ready_to_start.html, ready_to_submit.html |
N/A |
| Build Requirements | Same | Same |
| Runtime Config | Template context required | API response required |
| Modern/Legacy | Legacy (but still supported) | Modern (recommended) |
Important Notes
- Both approaches share the same build process - they both need:
workers.jsongenerated by Django- Webpack worker bundles built
-
webpack-worker-stats.jsoncreated -
The worker JavaScript is the same - only the delivery method differs
-
Both can coexist - LMS templates use JS 1.0, MFE uses JS 2.0
-
Neither eliminates build-time configuration - the JS bundle must be built at webpack time
Critical Understanding
The terms "JS 1.0" and "JS 2.0" do NOT refer to: - Different versions of the proctoring JavaScript code - Different webpack configurations - Different build processes
They refer ONLY to: - How the frontend obtains the worker bundle URL - Whether it's embedded in templates (1.0) or returned via API (2.0)
Both approaches require the same build-time steps:
1. Django must generate workers.json
2. Webpack must build worker bundles
3. webpack-worker-stats.json must be created
4. Worker bundles must be available at known URLs
The difference is purely in how the URL is delivered to the frontend at runtime.