diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0b94aaf7..9ffe6fb0 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -289,6 +289,8 @@ page.on("console", async e => { page.on("error", e => console.error("[Error]", e.message)); page.on("pageerror", e => { + if (e.message.includes("Sentry successfully disabled")) return; + if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { console.error("[Page Error]", e.message); report.otherErrors.push(e.message); diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index 26b36735..ef2849bb 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -18,6 +18,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType, StartAt } from "@utils/types"; const settings = definePluginSettings({ @@ -71,9 +72,64 @@ export default definePlugin({ startAt: StartAt.Init, start() { + // Sentry is initialized in its own WebpackInstance. + // It has everything it needs preloaded, so, it doesn't include any chunk loading functionality. + // Because of that, its WebpackInstance doesnt export wreq.m or wreq.c + + // To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set. + // When that happens we are gonna obtain a reference to its internal module cache (wreq.c) and proxy its prototype, + // so, when the first require to initialize the Sentry is attempted, we are gonna forcefully throw an error and abort everything. + Object.defineProperty(Function.prototype, "g", { + configurable: true, + + set(v: any) { + Object.defineProperty(this, "g", { + value: v, + configurable: true, + enumerable: true, + writable: true + }); + + // Ensure this is most likely the Sentry WebpackInstance. + // Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: ) to include it + const { stack } = new Error(); + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || this.c != null || !String(this).includes("exports:{}")) { + return; + } + + const cacheExtractSym = Symbol("vencord.cacheExtract"); + Object.defineProperty(Object.prototype, cacheExtractSym, { + configurable: true, + + get() { + // One more condition to check if this is the Sentry WebpackInstance + if (Array.isArray(this)) { + return { exports: {} }; + } + + new Logger("NoTrack", "#8caaee").info("Disabling Sentry by proxying its WebpackInstance cache"); + Object.setPrototypeOf(this, new Proxy(this, { + get() { + throw new Error("Sentry successfully disabled"); + } + })); + + Reflect.deleteProperty(Object.prototype, cacheExtractSym); + Reflect.deleteProperty(window, "DiscordSentry"); + return { exports: {} }; + } + }); + + // WebpackRequire our fake module id + this(cacheExtractSym); + } + }); + Object.defineProperty(window, "DiscordSentry", { configurable: true, + set() { + new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry"); Reflect.deleteProperty(window, "DiscordSentry"); } }); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index bd47cefa..4b3b28a8 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -56,58 +56,56 @@ Object.defineProperty(window, WEBPACK_CHUNK, { // normally, this is populated via webpackGlobal.push, which we patch below. // However, Discord has their .m prepopulated. // Thus, we use this hack to immediately access their wreq.m and patch all already existing factories -// -// Update: Discord now has TWO webpack instances. Their normal one and sentry -// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules Object.defineProperty(Function.prototype, "m", { configurable: true, set(v: any) { - // When using react devtools or other extensions, we may also catch their webpack here. - // This ensures we actually got the right one - const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(v)) { - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; - - logger.info("Found Webpack module factory", fileName); - patchFactories(v); - - // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. - // So if the setter is called, this means we can initialize the internal references to WebpackRequire. - Object.defineProperty(this, "p", { - configurable: true, - - set(this: WebpackInstance, bundlePath: string) { - Object.defineProperty(this, "p", { - value: bundlePath, - configurable: true, - enumerable: true, - writable: true - }); - - clearTimeout(setterTimeout); - - if (bundlePath !== "/assets/") return; - - logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); - _initWebpack(this); - - for (const beforeInitListener of beforeInitListeners) { - beforeInitListener(this); - } - } - }); - // setImmediate to clear this property setter if this is not the main Webpack. - // If this is the main Webpack, wreq.p will always be set before the timeout runs. - const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); - } - Object.defineProperty(this, "m", { value: v, configurable: true, enumerable: true, writable: true }); + + // When using react devtools or other extensions, we may also catch their webpack here. + // This ensures we actually got the right one + const { stack } = new Error(); + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) { + return; + } + + const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; + logger.info("Found Webpack module factory", fileName); + + patchFactories(v); + + // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. + // So if the setter is called, this means we can initialize the internal references to WebpackRequire. + Object.defineProperty(this, "p", { + configurable: true, + + set(this: WebpackInstance, bundlePath: string) { + Object.defineProperty(this, "p", { + value: bundlePath, + configurable: true, + enumerable: true, + writable: true + }); + + clearTimeout(setterTimeout); + if (bundlePath !== "/assets/") return; + + logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); + _initWebpack(this); + + for (const beforeInitListener of beforeInitListeners) { + beforeInitListener(this); + } + } + }); + // setImmediate to clear this property setter if this is not the main Webpack. + // If this is the main Webpack, wreq.p will always be set before the timeout runs. + const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); } });