From f3ceae67546a361f39fe170abcddd611c5ed518d Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Sat, 18 Apr 2026 19:14:03 -0300 Subject: [PATCH] feat: add tutor-contrib-google-analytics plugin Duplicates the GoogleAnalyticsLoader from frontend-platform and registers it for all MFEs via tutor-mfe's EXTERNAL_SCRIPTS hook. Co-Authored-By: Claude --- .../tutor-contrib-google-analytics/README.rst | 73 +++++++++++++++++++ .../tutor-contrib-google-analytics/setup.py | 58 +++++++++++++++ .../tutor_google_analytics/__about__.py | 1 + .../tutor_google_analytics/__init__.py | 0 .../tutor_google_analytics/plugin.py | 66 +++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 plugins/tutor-contrib-google-analytics/README.rst create mode 100644 plugins/tutor-contrib-google-analytics/setup.py create mode 100644 plugins/tutor-contrib-google-analytics/tutor_google_analytics/__about__.py create mode 100644 plugins/tutor-contrib-google-analytics/tutor_google_analytics/__init__.py create mode 100644 plugins/tutor-contrib-google-analytics/tutor_google_analytics/plugin.py diff --git a/plugins/tutor-contrib-google-analytics/README.rst b/plugins/tutor-contrib-google-analytics/README.rst new file mode 100644 index 0000000..32a6018 --- /dev/null +++ b/plugins/tutor-contrib-google-analytics/README.rst @@ -0,0 +1,73 @@ +Google Analytics plugin for `Tutor `__ +======================================================================= + +This plugin wires a Google Analytics 4 loader into every Open edX MFE that +is built through ``tutor-mfe``. It duplicates the ``GoogleAnalyticsLoader`` +that used to ship with ``@openedx/frontend-platform``. + +The loader reads ``GOOGLE_ANALYTICS_4_ID`` from the MFE runtime configuration +(or, for the frontend-base site, from ``commonAppConfig``). When that value +is unset, the loader is a no-op, so the plugin is safe to enable even if +Google Analytics has not yet been configured for a deployment. + +Implementation +-------------- + +The plugin declares a single ``GoogleAnalyticsLoader`` JavaScript class and +inlines it into both build pipelines that tutor-mfe supports. The class +body is injected into ``env.config.jsx`` via the +``mfe-env-config-buildtime-definitions`` patch (for legacy MFEs) and into +``customApp.tsx`` via the ``mfe-site-custom-app-definitions`` patch (for +the frontend-base site). A single ``tutormfe.hooks.EXTERNAL_SCRIPTS`` +registration targeting ``"all"`` then wires the class into both: each +legacy MFE's ``externalScripts`` array and the site's +``customApp.externalScripts``. + +At runtime, each pipeline instantiates the loader with the relevant app +configuration. Setting ``GOOGLE_ANALYTICS_4_ID`` in the MFE runtime config +(for legacy MFEs) or in ``FRONTEND_SITE_CONFIG['commonAppConfig']`` (for +the frontend-base site) is enough to activate tracking. Without a value, +the loader is a safe no-op. + +Installation +------------ + +Install directly from Github:: + + pip install git+https://github.com/openedx/openedx-tutor-plugins.git#subdirectory=plugins/tutor-contrib-google-analytics + +Alternatively, clone the parent repository locally and install it from the +checkout:: + + git clone https://github.com/openedx/openedx-tutor-plugins.git + cd openedx-tutor-plugins/plugins/tutor-contrib-google-analytics + pip install -e . + +Usage +----- + +Enable the plugin:: + + tutor plugins enable google-analytics + +Then rebuild the MFE images and restart the environment so the new +``env.config.jsx`` takes effect:: + + tutor images build mfe mfe-dev + tutor local launch + +Make sure that ``GOOGLE_ANALYTICS_4_ID`` is present in the MFE runtime +configuration for the deployments where tracking should be active. + +Uninstallation +-------------- + +To disable the plugin:: + + tutor plugins disable google-analytics + tutor local stop && tutor local start -d + +License +------- + +This software is licensed under the terms of the AGPLv3. diff --git a/plugins/tutor-contrib-google-analytics/setup.py b/plugins/tutor-contrib-google-analytics/setup.py new file mode 100644 index 0000000..977518f --- /dev/null +++ b/plugins/tutor-contrib-google-analytics/setup.py @@ -0,0 +1,58 @@ +import io +import os +from setuptools import setup, find_packages + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def load_readme(): + with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f: + return f.read() + + +def load_about(): + about = {} + with io.open( + os.path.join(HERE, "tutor_google_analytics", "__about__.py"), + "rt", + encoding="utf-8", + ) as f: + exec(f.read(), about) # pylint: disable=exec-used + return about + + +ABOUT = load_about() + + +setup( + name="tutor-contrib-google-analytics", + version=ABOUT["__version__"], + url="https://github.com/openedx/openedx-tutor-plugins", + project_urls={ + "Code": "https://github.com/openedx/openedx-tutor-plugins", + "Issue tracker": "https://github.com/openedx/openedx-tutor-plugins/issues", + }, + license="AGPLv3", + author="Adolfo R. Brandes", + description="Google Analytics plugin for Tutor", + long_description=load_readme(), + packages=find_packages(exclude=["tests*"]), + include_package_data=True, + python_requires=">=3.11", + install_requires=["tutor>=21.0.0", "tutor-mfe>=21.0.0"], + extras_require={"dev": ["tutor[dev]>=21.0.0"]}, + entry_points={ + "tutor.plugin.v1": [ + "google-analytics = tutor_google_analytics.plugin" + ] + }, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) diff --git a/plugins/tutor-contrib-google-analytics/tutor_google_analytics/__about__.py b/plugins/tutor-contrib-google-analytics/tutor_google_analytics/__about__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/plugins/tutor-contrib-google-analytics/tutor_google_analytics/__about__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/plugins/tutor-contrib-google-analytics/tutor_google_analytics/__init__.py b/plugins/tutor-contrib-google-analytics/tutor_google_analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/tutor-contrib-google-analytics/tutor_google_analytics/plugin.py b/plugins/tutor-contrib-google-analytics/tutor_google_analytics/plugin.py new file mode 100644 index 0000000..ce5c393 --- /dev/null +++ b/plugins/tutor-contrib-google-analytics/tutor_google_analytics/plugin.py @@ -0,0 +1,66 @@ +from tutor import hooks +from tutormfe.hooks import EXTERNAL_SCRIPTS + + +GOOGLE_ANALYTICS_LOADER = """ +class GoogleAnalyticsLoader { + constructor({ config }) { + this.analyticsId = config.GOOGLE_ANALYTICS_4_ID; + } + + loadScript() { + if (!this.analyticsId) { + return; + } + + global.googleAnalytics = global.googleAnalytics || []; + const { googleAnalytics } = global; + + // If the snippet was invoked do nothing. + if (googleAnalytics.invoked) { + return; + } + + // Invoked flag, to make sure the snippet + // is never invoked twice. + googleAnalytics.invoked = true; + + googleAnalytics.load = (key, options) => { + const scriptSrc = document.createElement('script'); + scriptSrc.type = 'text/javascript'; + scriptSrc.async = true; + scriptSrc.src = `https://www.googletagmanager.com/gtag/js?id=${key}`; + + const scriptGtag = document.createElement('script'); + scriptGtag.innerHTML = ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '${key}'); + `; + + // Insert our scripts next to the first script element. + const first = document.getElementsByTagName('script')[0]; + first.parentNode.insertBefore(scriptSrc, first); + first.parentNode.insertBefore(scriptGtag, first); + googleAnalytics._loadOptions = options; // eslint-disable-line no-underscore-dangle + }; + + // Load GoogleAnalytics with your key. + googleAnalytics.load(this.analyticsId); + } +} +""" + + +# Inline the loader into both build pipelines: env.config.jsx for legacy MFEs, +# customApp.tsx for the frontend-base site. +hooks.Filters.ENV_PATCHES.add_item( + ("mfe-env-config-buildtime-definitions", GOOGLE_ANALYTICS_LOADER) +) +hooks.Filters.ENV_PATCHES.add_item( + ("mfe-site-custom-app-definitions", GOOGLE_ANALYTICS_LOADER) +) + +# Register the loader for both targets ("all" covers legacy MFEs and the site). +EXTERNAL_SCRIPTS.add_item(("all", "GoogleAnalyticsLoader"))