Skip to content

Commit 1728af7

Browse files
arbrandesclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 8cc1518 commit 1728af7

5 files changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
Google Analytics plugin for `Tutor <https://docs.tutor.overhang.io>`__
2+
=======================================================================
3+
4+
This plugin wires a Google Analytics 4 loader into every Open edX MFE that
5+
is built through ``tutor-mfe``. It duplicates the ``GoogleAnalyticsLoader``
6+
that used to ship with ``@openedx/frontend-platform``.
7+
8+
The loader reads ``GOOGLE_ANALYTICS_4_ID`` from the MFE runtime configuration
9+
(or, for the frontend-base site, from ``commonAppConfig``). When that value
10+
is unset, the loader is a no-op, so the plugin is safe to enable even if
11+
Google Analytics has not yet been configured for a deployment.
12+
13+
Implementation
14+
--------------
15+
16+
The plugin declares a single ``GoogleAnalyticsLoader`` JavaScript class and
17+
inlines it into both build pipelines that tutor-mfe supports:
18+
19+
- Legacy MFEs: the class is injected into ``env.config.jsx`` via the
20+
``mfe-env-config-buildtime-definitions`` patch and registered for all
21+
MFEs through ``tutormfe.hooks.EXTERNAL_SCRIPTS``.
22+
- frontend-base site: the same class is injected into ``customApp.tsx``
23+
via the ``mfe-site-custom-app-definitions`` patch and registered through
24+
``tutormfe.hooks.FRONTEND_SCRIPTS``, which attaches it to the site's
25+
``customApp.externalScripts`` at build time.
26+
27+
At runtime, each pipeline instantiates the loader with the relevant app
28+
configuration. Setting ``GOOGLE_ANALYTICS_4_ID`` in the MFE runtime config
29+
(for legacy MFEs) or in ``FRONTEND_SITE_CONFIG['commonAppConfig']`` (for
30+
the frontend-base site) is enough to activate tracking. Without a value,
31+
the loader is a safe no-op.
32+
33+
Installation
34+
------------
35+
36+
Install directly from Github::
37+
38+
pip install git+https://github.com/openedx/openedx-tutor-plugins.git#subdirectory=plugins/tutor-contrib-google-analytics
39+
40+
Alternatively, clone the parent repository locally and install it from the
41+
checkout::
42+
43+
git clone https://github.com/openedx/openedx-tutor-plugins.git
44+
cd openedx-tutor-plugins/plugins/tutor-contrib-google-analytics
45+
pip install -e .
46+
47+
Usage
48+
-----
49+
50+
Enable the plugin::
51+
52+
tutor plugins enable google-analytics
53+
54+
Then rebuild the MFE images and restart the environment so the new
55+
``env.config.jsx`` takes effect::
56+
57+
tutor images build mfe mfe-dev
58+
tutor local launch
59+
60+
Make sure that ``GOOGLE_ANALYTICS_4_ID`` is present in the MFE runtime
61+
configuration for the deployments where tracking should be active.
62+
63+
Uninstallation
64+
--------------
65+
66+
To disable the plugin::
67+
68+
tutor plugins disable google-analytics
69+
tutor local stop && tutor local start -d
70+
71+
License
72+
-------
73+
74+
This software is licensed under the terms of the AGPLv3.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import io
2+
import os
3+
from setuptools import setup, find_packages
4+
5+
HERE = os.path.abspath(os.path.dirname(__file__))
6+
7+
8+
def load_readme():
9+
with io.open(os.path.join(HERE, "README.rst"), "rt", encoding="utf8") as f:
10+
return f.read()
11+
12+
13+
def load_about():
14+
about = {}
15+
with io.open(
16+
os.path.join(HERE, "tutor_google_analytics", "__about__.py"),
17+
"rt",
18+
encoding="utf-8",
19+
) as f:
20+
exec(f.read(), about) # pylint: disable=exec-used
21+
return about
22+
23+
24+
ABOUT = load_about()
25+
26+
27+
setup(
28+
name="tutor-contrib-google-analytics",
29+
version=ABOUT["__version__"],
30+
url="https://github.com/openedx/openedx-tutor-plugins",
31+
project_urls={
32+
"Code": "https://github.com/openedx/openedx-tutor-plugins",
33+
"Issue tracker": "https://github.com/openedx/openedx-tutor-plugins/issues",
34+
},
35+
license="AGPLv3",
36+
author="Adolfo R. Brandes",
37+
description="Google Analytics plugin for Tutor",
38+
long_description=load_readme(),
39+
packages=find_packages(exclude=["tests*"]),
40+
include_package_data=True,
41+
python_requires=">=3.11",
42+
install_requires=["tutor>=21.0.0", "tutor-mfe>=21.0.0"],
43+
extras_require={"dev": ["tutor[dev]>=21.0.0"]},
44+
entry_points={
45+
"tutor.plugin.v1": [
46+
"google-analytics = tutor_google_analytics.plugin"
47+
]
48+
},
49+
classifiers=[
50+
"Development Status :: 3 - Alpha",
51+
"Intended Audience :: Developers",
52+
"License :: OSI Approved :: GNU Affero General Public License v3",
53+
"Operating System :: OS Independent",
54+
"Programming Language :: Python",
55+
"Programming Language :: Python :: 3.11",
56+
"Programming Language :: Python :: 3.12",
57+
],
58+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

plugins/tutor-contrib-google-analytics/tutor_google_analytics/__init__.py

Whitespace-only changes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from tutor import hooks
2+
from tutormfe.hooks import EXTERNAL_SCRIPTS, FRONTEND_SCRIPTS
3+
4+
5+
GOOGLE_ANALYTICS_LOADER = """
6+
class GoogleAnalyticsLoader {
7+
constructor({ config }) {
8+
this.analyticsId = config.GOOGLE_ANALYTICS_4_ID;
9+
}
10+
11+
loadScript() {
12+
if (!this.analyticsId) {
13+
return;
14+
}
15+
16+
global.googleAnalytics = global.googleAnalytics || [];
17+
const { googleAnalytics } = global;
18+
19+
// If the snippet was invoked do nothing.
20+
if (googleAnalytics.invoked) {
21+
return;
22+
}
23+
24+
// Invoked flag, to make sure the snippet
25+
// is never invoked twice.
26+
googleAnalytics.invoked = true;
27+
28+
googleAnalytics.load = (key, options) => {
29+
const scriptSrc = document.createElement('script');
30+
scriptSrc.type = 'text/javascript';
31+
scriptSrc.async = true;
32+
scriptSrc.src = `https://www.googletagmanager.com/gtag/js?id=${key}`;
33+
34+
const scriptGtag = document.createElement('script');
35+
scriptGtag.innerHTML = `
36+
window.dataLayer = window.dataLayer || [];
37+
function gtag(){dataLayer.push(arguments);}
38+
gtag('js', new Date());
39+
gtag('config', '${key}');
40+
`;
41+
42+
// Insert our scripts next to the first script element.
43+
const first = document.getElementsByTagName('script')[0];
44+
first.parentNode.insertBefore(scriptSrc, first);
45+
first.parentNode.insertBefore(scriptGtag, first);
46+
googleAnalytics._loadOptions = options; // eslint-disable-line no-underscore-dangle
47+
};
48+
49+
// Load GoogleAnalytics with your key.
50+
googleAnalytics.load(this.analyticsId);
51+
}
52+
}
53+
"""
54+
55+
56+
# Legacy MFEs (frontend-platform): inline the loader into env.config.jsx.
57+
hooks.Filters.ENV_PATCHES.add_item(
58+
("mfe-env-config-buildtime-definitions", GOOGLE_ANALYTICS_LOADER)
59+
)
60+
EXTERNAL_SCRIPTS.add_item(("all", "GoogleAnalyticsLoader"))
61+
62+
63+
# frontend-base site: inline the loader into customApp and register it.
64+
hooks.Filters.ENV_PATCHES.add_item(
65+
("mfe-site-custom-app-definitions", GOOGLE_ANALYTICS_LOADER)
66+
)
67+
FRONTEND_SCRIPTS.add_item("GoogleAnalyticsLoader")

0 commit comments

Comments
 (0)