diff --git a/src/html/offscreen.html b/src/html/offscreen.html index 177e040d..a07f4aaa 100644 --- a/src/html/offscreen.html +++ b/src/html/offscreen.html @@ -1,2 +1,3 @@ + \ No newline at end of file diff --git a/src/scripts/background.ts b/src/scripts/background.ts index 22ebd5ad..df155696 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -1,7 +1,7 @@ import "webext-dynamic-content-scripts"; import addDomainPermissionToggle from "webext-permission-toggle"; -import { trackEvent } from "./utils/analytics"; +import { getAnalyticsUserId } from "./utils/analytics"; import { DISCORD_URL, EXTENSION_NAME, EXTENSION_WEBSITE } from "./utils/constants"; import { getBrowser } from "./utils/dom"; import { Logger } from "./utils/logger"; @@ -56,7 +56,7 @@ async function onActionClicked() { let badgeText = await chrome.action.getBadgeText({}); let n = Number.parseInt(badgeText); - trackEvent("button_click", { + trackAnalyticsEvent("button_click", { id: "main-browser-action-button", context: "Browser Action", value: String(n || 0), @@ -73,7 +73,7 @@ async function onActionClicked() { function onNotificationClicked(id: string) { Logger.log("Notification clicked"); - trackEvent("perform_action", { + trackAnalyticsEvent("perform_action", { id: "click", context: "Notifications", value: id, @@ -110,7 +110,7 @@ async function onAlarm(alarm: chrome.alarms.Alarm) { } function onInstalled(details: chrome.runtime.InstalledDetails) { - trackEvent("perform_action", { + trackAnalyticsEvent("perform_action", { id: "runtime_oninstalled", value: details.reason, context: "Versions", @@ -242,13 +242,7 @@ async function checkForNotifications() { // Do below only in Chrome // Create an offscreen document if one doesn't exist yet - if (!(await hasOffscreenDocument())) { - await chrome.offscreen.createDocument({ - url: OFFSCREEN_DOCUMENT_PATH, - reasons: [chrome.offscreen.Reason.DOM_PARSER], - justification: "Parse Schoology notifications, which are returned from the API as HTML", - }); - } + await createOffscreenDocument(); // Now that we have an offscreen document, we can dispatch the // message. chrome.runtime.sendMessage({ @@ -260,6 +254,21 @@ async function checkForNotifications() { declare var clients: { matchAll: () => Promise<{ url: string }[]> }; +async function createOffscreenDocument() { + try { + if (!(await hasOffscreenDocument())) { + await chrome.offscreen.createDocument({ + url: OFFSCREEN_DOCUMENT_PATH, + reasons: [chrome.offscreen.Reason.DOM_PARSER], + justification: + "Parse Schoology notifications, which are returned from the API as HTML", + }); + } + } catch (e) { + Logger.warn("Error creating offscreen document, it probably already exists", e); + } +} + async function hasOffscreenDocument() { // Check all windows controlled by the service worker if one of them is the offscreen document const matchedClients = await clients.matchAll(); @@ -271,4 +280,25 @@ async function hasOffscreenDocument() { return false; } +async function trackAnalyticsEvent(name: string, props: any) { + await createOffscreenDocument(); + let storageContents = await chrome.storage.sync.get(null); + chrome.runtime.sendMessage({ + type: "offscreen-analytics", + target: "offscreen", + data: { + name, + props, + settings: { + analytics: storageContents.analytics, + theme: storageContents.theme, + beta: storageContents.beta, + version: chrome.runtime.getManifest().version, + newVersion: storageContents.newVersion, + randomUserId: getAnalyticsUserId(), + }, + }, + }); +} + load(); diff --git a/src/scripts/content.ts b/src/scripts/content.ts index 91fbfa32..3f97b079 100644 --- a/src/scripts/content.ts +++ b/src/scripts/content.ts @@ -1,6 +1,7 @@ import * as pages from "./pages"; import * as utils from "./utils"; -import { initializeAnalytics } from "./utils/analytics"; +import { getAnalyticsUserId, initializeAnalytics } from "./utils/analytics"; +import { getBrowser } from "./utils/dom"; import { Setting, generateDebugInfo } from "./utils/settings"; declare global { @@ -31,8 +32,20 @@ function ready() { } async function load() { - await initializeAnalytics(); await pages.all.preload(); + + await initializeAnalytics({ + documentContext: true, + isAnalyticsEnabled: + getBrowser() !== "Firefox" && Setting.getValue("analytics") === "enabled", + selectedTheme: Setting.getValue("theme", ""), + selectedBeta: Setting.getValue("beta", ""), + currentVersion: chrome.runtime.getManifest().version, + newVersion: Setting.getValue("newVersion", ""), + randomUserId: await getAnalyticsUserId(), + themeIsModern: document.documentElement.getAttribute("modern") ?? "false", + }); + await ready(); await pages.all.load(); diff --git a/src/scripts/offscreen.ts b/src/scripts/offscreen.ts index ad39444a..51db83b9 100644 --- a/src/scripts/offscreen.ts +++ b/src/scripts/offscreen.ts @@ -1,9 +1,12 @@ +import { initializeAnalytics, trackEvent } from "./utils/analytics"; +import { getBrowser } from "./utils/dom"; import { loadAssignmentNotifications } from "./utils/notifications"; // Registering this listener when the script is first executed ensures that the // offscreen document will be able to receive messages when the promise returned // by `offscreen.createDocument()` resolves. chrome.runtime.onMessage.addListener(handleMessages); +let analyticsIsEnabled = false; // This function performs basic filtering and error checking on messages before // dispatching the message to a more specific message handler. @@ -33,6 +36,25 @@ async function handleMessages( }, }); break; + case "offscreen-analytics": + if (!analyticsIsEnabled) { + await initializeAnalytics({ + documentContext: false, + isAnalyticsEnabled: + getBrowser() !== "Firefox" && + message.data.settings.analytics !== "disabled", + selectedTheme: message.data.settings.theme ?? "", + selectedBeta: message.data.settings.beta ?? "", + currentVersion: message.data.settings.version, + newVersion: message.data.settings.newVersion ?? "", + randomUserId: message.data.settings.randomUserId, + themeIsModern: "", + }); + analyticsIsEnabled = true; + } + + trackEvent(message.data.name, message.data.props); + break; default: console.warn(`Unexpected message type received: '${message.type}'.`); return false; diff --git a/src/scripts/theme-editor.ts b/src/scripts/theme-editor.ts index 04e64798..d8c28bc5 100644 --- a/src/scripts/theme-editor.ts +++ b/src/scripts/theme-editor.ts @@ -2,11 +2,11 @@ import $ from "jquery"; import M from "materialize-css"; import "spectrum-colorpicker"; -import { initializeAnalytics, trackEvent } from "./utils/analytics"; +import { getAnalyticsUserId, initializeAnalytics, trackEvent } from "./utils/analytics"; import { DEFAULT_THEME_NAME } from "./utils/constants"; import { DEFAULT_ICONS } from "./utils/default-icons"; import { CLASSIC_THEMES, DEFAULT_THEMES, LAUSD_THEMES } from "./utils/default-themes"; -import { DeepPartial, createElement, setCSSVariable } from "./utils/dom"; +import { DeepPartial, createElement, getBrowser, setCSSVariable } from "./utils/dom"; import { Logger } from "./utils/logger"; import { CustomColorDefinition, @@ -49,9 +49,19 @@ declare global { } async function load() { - initializeAnalytics(); - __storage = await chrome.storage.sync.get(null); + + await initializeAnalytics({ + documentContext: true, + isAnalyticsEnabled: getBrowser() !== "Firefox" && __storage.analytics !== "disabled", + selectedTheme: __storage.theme ?? "", + selectedBeta: __storage.beta ?? "", + currentVersion: chrome.runtime.getManifest().version, + newVersion: __storage.newVersion ?? "", + randomUserId: await getAnalyticsUserId(), + themeIsModern: document.documentElement.getAttribute("modern") ?? "false", + }); + defaultDomain = __storage.defaultDomain || "app.schoology.com"; if (isLAUSD()) { diff --git a/src/scripts/utils/analytics.ts b/src/scripts/utils/analytics.ts index 460011e4..82ef5cd9 100644 --- a/src/scripts/utils/analytics.ts +++ b/src/scripts/utils/analytics.ts @@ -34,7 +34,7 @@ export var trackEvent = function ( console.debug("[S+] Tracking disabled by user", arguments); }; -export async function initializeAnalytics() { +export async function getAnalyticsUserId() { function getRandomToken() { // E.g. 8 * 32 = 256 bits token var randomPool = new Uint8Array(32); @@ -47,31 +47,41 @@ export async function initializeAnalytics() { return hex; } - let s = await chrome.storage.sync.get({ - analytics: getBrowser() === "Firefox" ? "disabled" : "enabled", - theme: "", - beta: "", - newVersion: "", - }); - - if (s.analytics === "enabled") { - let l = await chrome.storage.local.get({ randomUserId: null }); - - if (!l.randomUserId) { - let randomToken = getRandomToken(); - await chrome.storage.local.set({ randomUserId: randomToken }); - enableAnalytics(s.theme, s.beta, s.newVersion, randomToken); - } else { - enableAnalytics(s.theme, s.beta, s.newVersion, l.randomUserId); - } + let l: { randomUserId?: string } = await chrome.storage.local.get({ randomUserId: null }); + + if (!l.randomUserId) { + let randomUserId = getRandomToken(); + await chrome.storage.local.set({ randomUserId }); + return randomUserId; + } + + return l.randomUserId; +} + +export async function initializeAnalytics({ + documentContext, + isAnalyticsEnabled, + selectedTheme, + selectedBeta, + currentVersion, + newVersion, + randomUserId, + themeIsModern, +}: { + documentContext: boolean; + isAnalyticsEnabled: boolean; + selectedTheme: string | null; + selectedBeta: string | null; + currentVersion: string; + newVersion: string; + randomUserId: string; + themeIsModern: string; +}) { + if (isAnalyticsEnabled) { + enableAnalytics(); } - function enableAnalytics( - selectedTheme: string, - beta: string, - newVersion: string, - randomUserId: string - ) { + function enableAnalytics() { // Google Analytics v4 (globalThis as any).dataLayer = (globalThis as any).dataLayer || []; @@ -82,20 +92,22 @@ export async function initializeAnalytics() { gtag("js", new Date()); - gtag("config", "G-YM6B00RDYC", { + const gtagConfig = { page_location: location.href.replace(/\/\d{3,}\b/g, "/*"), page_path: location.pathname.replace(/\/\d{3,}\b/g, "/*"), page_title: null, user_id: randomUserId, user_properties: { - extensionVersion: chrome.runtime.getManifest().version, - domain: location.host, theme: selectedTheme, - modernTheme: document.documentElement.getAttribute("modern"), - activeBeta: beta, + activeBeta: selectedBeta, lastEnabledVersion: newVersion, + extensionVersion: currentVersion, + domain: location.host, + modernTheme: themeIsModern, }, - }); + }; + + gtag("config", "G-YM6B00RDYC", gtagConfig); trackEvent = function ( eventName, @@ -135,37 +147,41 @@ export async function initializeAnalytics() { }); } - let trackedElements = new Set(); - let observer = new MutationObserver((mutations, mutationObserver) => { - for (let elem of document.querySelectorAll(".splus-track-clicks:not(.splus-tracked)")) { - if (!trackedElements.has(elem)) { - elem.addEventListener("click", trackClick); - elem.addEventListener("auxclick", trackClick); - elem.classList.add("splus-tracked"); - trackedElements.add(elem); + if (documentContext) { + let trackedElements = new Set(); + let observer = new MutationObserver((mutations, mutationObserver) => { + for (let elem of document.querySelectorAll( + ".splus-track-clicks:not(.splus-tracked)" + )) { + if (!trackedElements.has(elem)) { + elem.addEventListener("click", trackClick); + elem.addEventListener("auxclick", trackClick); + elem.classList.add("splus-tracked"); + trackedElements.add(elem); + } } - } - }); - - var readyStateCheckInterval = setInterval(function () { - if (document.readyState === "complete") { - clearInterval(readyStateCheckInterval); - init(); - } - }, 10); - - function init() { - observer.observe(document.body, { - childList: true, - subtree: true, }); - for (let elem of document.querySelectorAll(".splus-track-clicks")) { - if (!trackedElements.has(elem)) { - elem.addEventListener("click", trackClick); - elem.addEventListener("auxclick", trackClick); - elem.classList.add("splus-tracked"); - trackedElements.add(elem); + var readyStateCheckInterval = setInterval(function () { + if (document.readyState === "complete") { + clearInterval(readyStateCheckInterval); + init(); + } + }, 10); + + function init() { + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + for (let elem of document.querySelectorAll(".splus-track-clicks")) { + if (!trackedElements.has(elem)) { + elem.addEventListener("click", trackClick); + elem.addEventListener("auxclick", trackClick); + elem.classList.add("splus-tracked"); + trackedElements.add(elem); + } } } }