Plugin
NoTrack
Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting
1
import { definePluginSettings } from "@api/Settings";2
import { Devs } from "@utils/constants";3
import { Logger } from "@utils/Logger";4
import definePlugin, { OptionType, StartAt } from "@utils/types";5
import { WebpackRequire } from "@vencord/discord-types/webpack";6
7
const settings = definePluginSettings({8
disableAnalytics: {9
type: OptionType.BOOLEAN,10
description: "Disable Discord039;s tracking (analytics/039;science039;)",11
default: true,12
restartNeeded: true13
}14
});15
16
export default definePlugin({17
name: "NoTrack",18
description: "Disable Discord039;s tracking (analytics/039;science039;), metrics and Sentry crash reporting",19
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],20
required: true,21
22
settings,23
24
patches: [25
{26
find: "AnalyticsActionHandlers.handle",27
predicate: () => settings.store.disableAnalytics,28
replacement: {29
match: /\(0,\i\.analyticsTrackingStoreMaker\)/,30
replace: "(()=>{})",31
},32
},33
{34
find: ".METRICS_V2",35
replacement: [36
{37
match: /this\._intervalId=/,38
replace: "this._intervalId=void 0&&"39
},40
{41
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,42
replace: "$&return;"43
}44
]45
},46
{47
find: ".BetterDiscord||null!=",48
replacement: {49
// Make hasClientMods return false50
match: /(?=let \i=window;)/,51
replace: "return false;"52
}53
}54
],55
56
// The TRACK event takes an optional `resolve` property that is called when the tracking event was submitted to the server.57
// A few spots in Discord await this callback before continuing (most notably the Voice Debug Logging toggle).58
// Since we NOOP the AnalyticsActionHandlers module, there is no handler for the TRACK event, so we have to handle it ourselves59
flux: {60
TRACK(event) {61
event?.resolve?.();62
}63
},64
65
startAt: StartAt.Init,66
start() {67
// Sentry is initialized in its own WebpackInstance.68
// It has everything it needs preloaded, so, it doesn't include any chunk loading functionality.69
// Because of that, its WebpackInstance doesnt export wreq.m or wreq.c70
71
// To circuvent this and disable Sentry we are gonna hook when wreq.d of its WebpackInstance is set.72
// When that happens we are gonna forcefully throw an error and abort everything.73
Object.defineProperty(Function.prototype, "d", {74
configurable: true,75
76
set(this: WebpackRequire, esmDeclareFunc: WebpackRequire["d"]) {77
Object.defineProperty(this, "d", {78
value: esmDeclareFunc,79
configurable: true,80
enumerable: true,81
writable: true82
});83
84
// Ensure this is most likely the Sentry WebpackInstance.85
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it86
const { stack } = new Error();87
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {88
return;89
}90
91
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];92
if (!assetPath) {93
return;94
}95
96
const srcRequest = new XMLHttpRequest();97
srcRequest.open("GET", assetPath, false);98
srcRequest.send();99
100
// Final condition to see if this is the Sentry WebpackInstance101
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies102
if (!srcRequest.responseText.includes(".DiscordSentry=")) {103
return;104
}105
106
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by erroring its WebpackInstance");107
108
Reflect.deleteProperty(Function.prototype, "d");109
Reflect.deleteProperty(window, "DiscordSentry");110
111
throw new Error("Sentry successfully disabled");112
}113
});114
115
Object.defineProperty(window, "DiscordSentry", {116
configurable: true,117
118
set() {119
new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry");120
121
Reflect.deleteProperty(Function.prototype, "d");122
Reflect.deleteProperty(window, "DiscordSentry");123
}124
});125
}126
});127