Files
team/webapp/channels/webpack.config.js
650da01975 MM-64486: Remove telemetry (#33606)
* MM-64486: Remove telemetry

Remove telemetry from Mattermost. We're no longer relying on Rudder upstream, and no longer making use of this information.

* recover mock for SystemStore.Get

* Fix TestClearPushNotificationSync by adding missing SystemStore mock

The test was failing because the SystemStore mock was missing the Get()
method that's required by the ServerId() function. Added the missing mock
to return a StringMap with SystemServerId.

* fix mocking issue

* Remove now-unused telemetry and constants

* Remove "Disable telemetry events" debug setting

* Remove empty functions

* Remove most "Telemetry tracking removed" comments

* Remove remains of DataPrefetch telemetry

* Remove now-unused prop from InviteMembersButton

* Remove trackDotMenuEvent

* Remove some more leftover comments

* Remove lingering logic related to trackingLocation

* Remove now-unused argument from useCopyText

* Remove lingering telemetry references from PreparingWorkspace

* fixup Remove trackDotMenuEvent

* Remove lingering telemetry references from signup page and password check

* Update snapshots and fix test broken by my changes

* Fix unintended behavior change in thread list filtering

Remove handleSetFilter wrapper that was accidentally modified during
telemetry removal. The function was calling clear() when switching to
unread filter, which was not the original behavior. Use setFilter
directly instead, restoring the original functionality.

* Remove unused useOpenDowngradeModal hook

The useOpenDowngradeModal hook was not being used anywhere in the codebase.

* Remove unused expandableLink from useExpandOverageUsersCheck

The expandableLink return value was not being used by any components.

* Re-add missing TeamLinkClicked performance telemetry

The mark(Mark.TeamLinkClicked) call was accidentally removed from the
handleSwitch function. This telemetry is needed for Looker-based
performance tracking.

* drop LogSettings.VerboseDiagnostics

---------

Co-authored-by: Harrison Healey <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
2025-09-04 18:46:18 +00:00

495 lines
17 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console, no-process-env */
const path = require('path');
const url = require('url');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const webpack = require('webpack');
const {ModuleFederationPlugin} = require('webpack').container;
const WebpackPwaManifest = require('webpack-pwa-manifest');
const packageJson = require('./package.json');
const NPM_TARGET = process.env.npm_lifecycle_event;
const targetIsBuild = NPM_TARGET?.startsWith('build');
const targetIsRun = NPM_TARGET?.startsWith('run');
const targetIsStats = NPM_TARGET === 'stats';
const targetIsDevServer = NPM_TARGET?.startsWith('dev-server');
const targetIsEsLint = !targetIsBuild && !targetIsRun && !targetIsDevServer;
const DEV = targetIsRun || targetIsStats || targetIsDevServer;
const STANDARD_EXCLUDE = [
/node_modules/,
];
let publicPath = '/static/';
// Allow overriding the publicPath in dev from the exported SiteURL.
if (DEV) {
const siteURL = process.env.MM_SERVICESETTINGS_SITEURL || '';
if (siteURL) {
publicPath = path.join(new url.URL(siteURL).pathname, 'static') + '/';
}
}
// Track the build time so that we can bust any caches that may have incorrectly cached remote_entry.js from before we
// started setting Cache-Control: no-cache for that file on the server. This can be removed in 2024 after those cached
// entries are guaranteed to have expired.
const buildTimestamp = Date.now();
var config = {
entry: ['./src/root.tsx'],
output: {
publicPath,
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
assetModuleFilename: 'files/[contenthash][ext]',
clean: true,
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: STANDARD_EXCLUDE,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// Babel configuration is in .babelrc because jest requires it to be there.
},
},
},
{
test: /\.json$/,
include: [
path.resolve(__dirname, 'src/i18n'),
],
exclude: [/en\.json$/],
type: 'asset/resource',
generator: {
filename: 'i18n/[name].[contenthash].json',
},
},
{
test: /\.(css|scss)$/,
exclude: /\/highlight\.js\//,
use: [
DEV ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
],
},
{
test: /\.scss$/,
use: [
{
loader: 'sass-loader',
options: {
sassOptions: {
loadPaths: ['src/sass'],
},
},
},
],
},
{
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/,
type: 'asset/resource',
use: [
{
loader: 'image-webpack-loader',
options: {},
},
],
},
{
test: /\.apng$/,
type: 'asset/resource',
},
{
test: /\/highlight\.js\/.*\.css$/,
type: 'asset/resource',
},
],
},
resolve: {
modules: [
'node_modules',
'./src',
],
alias: {
'mattermost-redux/test': 'packages/mattermost-redux/test',
'mattermost-redux': 'packages/mattermost-redux/src',
'@mui/styled-engine': '@mui/styled-engine-sc',
// This alias restricts single version of styled components across all packages
'styled-components': path.resolve(__dirname, '..', 'node_modules', 'styled-components'),
},
extensions: ['.ts', '.tsx', '.js', '.jsx'],
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer/'),
},
},
performance: {
hints: 'warning',
},
target: 'web',
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].css',
}),
new HtmlWebpackPlugin({
filename: 'root.html',
inject: 'head',
template: 'src/root.html',
scriptLoading: 'blocking',
meta: {
csp: {
'http-equiv': 'Content-Security-Policy',
content: generateCSP(),
},
},
}),
new CopyWebpackPlugin({
patterns: [
{from: 'src/images/emoji', to: 'emoji'},
{from: 'src/images/img_trans.gif', to: 'images'},
{from: 'src/images/logo-email.png', to: 'images'},
{from: 'src/images/favicon', to: 'images/favicon'},
{from: 'src/images/appIcons.png', to: 'images'},
{from: 'src/images/logo-email.png', to: 'images'},
{from: 'src/images/browser-icons', to: 'images/browser-icons'},
{from: 'src/images/cloud', to: 'images'},
{from: 'src/images/welcome_illustration_new.png', to: 'images'},
{from: 'src/images/logo_email_blue.png', to: 'images'},
{from: 'src/images/logo_email_dark.png', to: 'images'},
{from: 'src/images/logo_email_gray.png', to: 'images'},
{from: 'src/images/forgot_password_illustration.png', to: 'images'},
{from: 'src/images/invite_illustration.png', to: 'images'},
{from: 'src/images/channel_icon.png', to: 'images'},
{from: 'src/images/c_avatar.png', to: 'images'},
{from: 'src/images/c_download.png', to: 'images'},
{from: 'src/images/c_socket.png', to: 'images'},
{from: 'src/images/admin-onboarding-background.jpg', to: 'images'},
{from: 'src/images/cloud-laptop.png', to: 'images'},
{from: 'src/images/cloud-laptop-error.png', to: 'images'},
{from: 'src/images/cloud-laptop-warning.png', to: 'images'},
{from: 'src/images/cloud-upgrade-person-hand-to-face.png', to: 'images'},
{from: 'src/images/payment_processing.png', to: 'images'},
{from: 'src/images/purchase_alert.png', to: 'images'},
{from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'},
{from: 'src/components/initial_loading_screen/initial_loading_screen.css', to: 'css'},
],
}),
// Generate manifest.json, honouring any configured publicPath. This also handles injecting
// <link rel="apple-touch-icon" ... /> and <meta name="apple-*" ... /> tags into root.html.
new WebpackPwaManifest({
name: 'Mattermost',
short_name: 'Mattermost',
start_url: '..',
description: 'Mattermost is an open source, self-hosted Slack-alternative',
background_color: '#ffffff',
inject: true,
ios: true,
fingerprints: false,
orientation: 'any',
filename: 'manifest.json',
icons: [{
src: path.resolve('src/images/favicon/android-chrome-192x192.png'),
type: 'image/png',
sizes: '192x192',
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-120x120.png'),
type: 'image/png',
sizes: '120x120',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-144x144.png'),
type: 'image/png',
sizes: '144x144',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-152x152.png'),
type: 'image/png',
sizes: '152x152',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-57x57.png'),
type: 'image/png',
sizes: '57x57',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-60x60.png'),
type: 'image/png',
sizes: '60x60',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-72x72.png'),
type: 'image/png',
sizes: '72x72',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-76x76.png'),
type: 'image/png',
sizes: '76x76',
ios: true,
}, {
src: path.resolve('src/images/favicon/favicon-16x16.png'),
type: 'image/png',
sizes: '16x16',
}, {
src: path.resolve('src/images/favicon/favicon-32x32.png'),
type: 'image/png',
sizes: '32x32',
}, {
src: path.resolve('src/images/favicon/favicon-96x96.png'),
type: 'image/png',
sizes: '96x96',
}],
}),
new MonacoWebpackPlugin({
languages: [],
// don't include features we disable. these generally correspond to the options
// passed to editor initialization in note-content-editor.tsx
// @see https://github.com/microsoft/monaco-editor/blob/main/webpack-plugin/README.md#options
features: [
'!bracketMatching',
'!codeAction',
'!codelens',
'!colorPicker',
'!comment',
'!diffEditor',
'!diffEditorBreadcrumbs',
'!folding',
'!gotoError',
'!gotoLine',
'!gotoSymbol',
'!gotoZoom',
'!inspectTokens',
'!multicursor',
'!parameterHints',
'!quickCommand',
'!quickHelp',
'!quickOutline',
'!referenceSearch',
'!rename',
'!snippet',
'!stickyScroll',
'!suggest',
'!toggleHighContrast',
'!unicodeHighlighter',
],
}),
],
};
function generateCSP() {
let csp = 'script-src \'self\' js.stripe.com/v3';
if (DEV) {
// Development source maps require eval
csp += ' \'unsafe-eval\'';
}
return csp;
}
async function initializeModuleFederation() {
function makeSharedModules(packageNames, singleton) {
const sharedObject = {};
for (const packageName of packageNames) {
const version = packageJson.dependencies[packageName];
sharedObject[packageName] = {
// Ensure only one copy of this package is ever loaded
singleton,
// Setting this to true causes the app to error out if the required version is not satisfied
strictVersion: singleton,
// Set these to match the specific version that the web app includes
requiredVersion: singleton ? version : undefined,
version,
};
}
return sharedObject;
}
async function getRemoteContainers() {
const products = [];
const remotes = {};
for (const product of products) {
remotes[product.name] = `${product.name}@[window.basename]/static/products/${product.name}/remote_entry.js?bt=${buildTimestamp}`;
}
return {remotes};
}
const {remotes} = await getRemoteContainers();
const moduleFederationPluginOptions = {
name: 'mattermost_webapp',
remotes,
shared: [
// Shared modules will be made available to other containers (ie products and plugins using module federation).
// To allow for better sharing, containers shouldn't require exact versions of packages like the web app does.
// Other containers will use these shared modules if their required versions match. If they don't match, the
// version packaged with the container will be used.
makeSharedModules([
'@mattermost/client',
'@mattermost/types',
'luxon',
], false),
// Other containers will be forced to use the exact versions of shared modules that the web app provides.
makeSharedModules([
'history',
'react',
'react-beautiful-dnd',
'react-bootstrap',
'react-dom',
'react-intl',
'react-redux',
'react-router-dom',
'styled-components',
], true),
],
};
// Desktop specific code for remote module loading
moduleFederationPluginOptions.exposes = {
'./app': 'components/app',
'./store': 'stores/redux_store',
'./styles': './src/sass/styles.scss',
'./registry': 'module_registry',
};
moduleFederationPluginOptions.filename = `remote_entry.js?bt=${buildTimestamp}`;
config.plugins.push(new ModuleFederationPlugin(moduleFederationPluginOptions));
// Add this plugin to perform the substitution of window.basename when loading remote containers
config.plugins.push(new ExternalTemplateRemotesPlugin());
config.plugins.push(new webpack.DefinePlugin({
REMOTE_CONTAINERS: JSON.stringify(remotes),
}));
}
if (DEV) {
// Development mode configuration
config.mode = 'development';
config.devtool = 'eval-cheap-module-source-map';
} else {
// Production mode configuration
config.mode = 'production';
config.devtool = 'source-map';
}
const env = {};
if (DEV) {
env.PUBLIC_PATH = JSON.stringify(publicPath);
} else {
env.NODE_ENV = JSON.stringify('production');
}
config.plugins.push(new webpack.DefinePlugin({
'process.env': env,
}));
if (targetIsDevServer) {
const proxyToServer = {
logLevel: 'silent',
target: process.env.MM_SERVICESETTINGS_SITEURL ?? 'http://localhost:8065',
xfwd: true,
};
config = {
...config,
devtool: 'eval-cheap-module-source-map',
devServer: {
liveReload: true,
proxy: [
{
context: '/api',
...proxyToServer,
ws: true,
},
{
context: '/plugins',
...proxyToServer,
},
{
context: '/static/plugins',
...proxyToServer,
},
],
port: 9005,
devMiddleware: {
writeToDisk: false,
},
historyApiFallback: {
index: '/static/root.html',
},
},
performance: false,
optimization: {
...config.optimization,
splitChunks: false,
},
};
}
// Export PRODUCTION_PERF_DEBUG=1 when running webpack to enable support for the react profiler
// even while generating production code. (Performance testing development code is typically
// not helpful.)
// See https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html and
// https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977
if (process.env.PRODUCTION_PERF_DEBUG) {
console.log('Enabling production performance debug settings'); //eslint-disable-line no-console
config.resolve.alias['react-dom'] = 'react-dom/profiling';
config.resolve.alias['schedule/tracing'] = 'schedule/tracing-profiling';
config.optimization = {
// Skip minification to make the profiled data more useful.
minimize: false,
};
}
if (targetIsEsLint) {
// ESLint can't handle setting an async config, so just skip the async part
module.exports = config;
} else {
module.exports = async () => {
// Do this asynchronously so we can determine whether which remote modules are available
await initializeModuleFederation();
return config;
};
}