Is Mixmax: AI-Powered Sales Engagement, Email Tracking and Meeting Scheduling safe?
Mixmax is high risk. Mixmax requested selector/watcher JSON from extension.mixmax.com seconds after loading Gmail. The same loader runs on Gmail, Salesforce, LinkedIn: it fetches two config files into page globals, then loads the module watching page elements.…
Who publishes itMixmax - no other listings under this identity
Mixmax - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Mixmax loads remote page rules on Gmail pages
Mixmax requested selector/watcher JSON from extension.mixmax.com seconds after loading Gmail.
The same loader runs on Gmail, Salesforce, LinkedIn: it fetches two config files into page globals, then loads the module watching page elements.
You open Gmail, Salesforce, or the LinkedIn sales widget while Mixmax is installed.
The content script maps the page to a build target such as gmail-inbox, salesforce, or linkedin.
The extension requests remote selector and watcher rules for that page.
On Gmail, dynamic analysis observed the watcher and selector JSON files load from extension.mixmax.com within seven seconds.
| Field | Value | Why it matters | |
|---|---|---|---|
Page target | gmail-inbox | Tells the extension which web app you are using so it can load a matching rule set. | |
Compose body selector | COMPOSE_DIALOG_BODY = .Am.Al.editable.LW-avf | Points the extension at the editable email body area in your Gmail compose window. | |
Message watcher tag | tag = onceOpenMessage; sources = message, expandedMessage | Names a page element state the extension should detect while you read messages. | |
Watch condition | attributeFilter = class; condition = has class h7 | Tells the extension which page changes should make a watched element count as active. | |
Finder interval | nativeComposeStandalone finder retries every 5000 ms | Controls how often the extension retries looking for selected Gmail elements. |
Content script builds the Mixmax config URLs and stores both responses on window
const SFDC_DOMAIN = /(^|\.)(?:sales)?force\.com$/;
const hostname = window.location.hostname;
const isGmail = hostname === "mail.google.com";
const isLinkedIn = hostname === "www.linkedin.com" && window.location.pathname.startsWith("/sales/widget");
let buildTarget;
if (isGmail) {
buildTarget = "gmail-inbox";
} else if (SFDC_DOMAIN.test(hostname)) {
buildTarget = "salesforce";
} else if (isLinkedIn) {
buildTarget = "linkedin";
}
const LOAD_FAILURE_WARN_ORIGINS = [ "https://mail.google.com" ];
function isValidWarningOrigin(origin) {
if (!origin.startsWith("https://")) return false;
if (LOAD_FAILURE_WARN_ORIGINS.includes(origin)) return true;
return SFDC_DOMAIN.test(origin);
}
const warnEnvironments = new Set([ "staging" ]);
const rValidEnvironment = /^[0-9a-z-]+$/i;
function getExtensionOrigin(environment) {
if (warnEnvironments.has(environment)) {
console.warn(`Mixmax extension is running from ${environment}.`);
}
if (!environment || environment === "production") {
return "https://extension.mixmax.com";
}
if (!rValidEnvironment.test(environment)) {
throw new Error("extension origin not valid");
}
if (Environment.previewBranch) {
return `https://extension-staging.mixmax.com/preview/${Environment.previewBranch}`;
}
return `https://extension-${environment}.mixmax.com`;
}
const GET_EVERYTHING = null;
chrome.storage.local.get(GET_EVERYTHING, (function(storage) {
const environment = storage.environment;
const remoteSource = getExtensionOrigin(environment) + "/src";
const remoteConfig = getExtensionOrigin(environment) + "/config";
const version = storage[`loadableCodeVersion_${environment}`] || "latest";
const extensionBaseUrl = chrome.runtime.getURL(`loadable/${version}`);
void loadLocal(extensionBaseUrl, remoteSource, remoteConfig);
}));
async function loadLocal(scriptBaseUrl, cssBaseUrl, configBaseUrl) {
try {
await Promise.all([ loadCSS(cssBaseUrl, 6), loadConfigs(configBaseUrl, 6) ]);
if (isGmail) {
await gmailPageInteropLoad;
}
const sourceSubdir = null;
await loadScript(scriptBaseUrl, sourceSubdir);
} catch (err) {
if (isValidWarningOrigin(window.location.origin)) {
alert("Failed to load extension locally. Check Chrome DevTools console for errors.");
}
throw err;
}
}
async function loadScript(origin, sourceSubdir, numRetries = 0) {
const maybeSourceSubdir = validateSourceSubdir(sourceSubdir) ? `${sourceSubdir}/` : "";
const jsURL = new URL(`${origin}/${maybeSourceSubdir}build-${buildTarget}.js`);
await retry((async num => {
const url = new URL(jsURL);
if (num) {
url.searchParams.set("v", `mixmax-extension-${num}`);
}
try {
await import(url.href);
} catch (err) {
if (err && err[Symbol.for("mixmaxErrorSource")] === "mixmax-extension-source") {
err.isPermanent = true;
}
throw err;
}
}), numRetries + 1);
}
async function fetchJSON(url) {
const res = await fetch(url, {
mode: "cors",
credentials: "omit",
redirect: "follow",
referrerPolicy: "origin"
});
if (!res.ok) {
throw new Error(`failed to fetch JSON from ${url}`);
}
return await res.json();
}
async function loadConfigs(origin, numRetries = 0) {
[window.SELECTORS, window.WATCHER_CONFIG] = await Promise.all([ retry((async () => await fetchJSON(`${origin}/${buildTarget}-selectors.json`)), numRetries + 1), retry((async () => await fetchJSON(`${origin}/${buildTarget}-watcher.json`)), numRetries + 1) ]);
}The Gmail module consumes the remote watcher object when it is present
if (window.WATCHER_CONFIG) {
e.tags = {}, e.watchers = [], e.finders = {};
for (const [t, n] of Object.entries(window.WATCHER_CONFIG)) {
const r = Ts(n, t);
Object.assign(e.tags, r.tags), e.watchers.push(...r.watchers), Object.assign(e.finders, r.finders)
}
window.WATCHER_CONFIG = {}
}Background service worker polls runtime configuration once per minute
async function checkRuntimeConfiguration() {
if (!navigator.onLine)
return;
// Cache bust the gate URL just in case the browser decides to cache.
const url = Environment.getExtensionUrl() + '/extension-runtime-config.json?cb=' + Date.now();
try {
const response = await fetch(url, { cache: 'no-cache' });
if (!response.ok) {
error_captureMessage('Non-200 response from S3', {
culprit: 'Querying the extension runtime configuration',
extra: {
status: response.status,
body: await response.text().catch((conversionError) => conversionError.message),
},
});
return;
}
const runtimeConfiguration = await response.json();
const runtimeConfigurationKey = `runtimeConfiguration_${Environment.get()}`;
// TODO: use promise API as soon as we're on Mv3
chrome.storage.local.get(runtimeConfigurationKey, (storage) => {
// The API docs for `chrome.storage.local.get` say `storage` should never be null,
// but we've found it sometimes (though rarely) is.
if (!lodash_default().isEqual(runtimeConfiguration, storage === null || storage === void 0 ? void 0 : storage[runtimeConfigurationKey])) {
void setStorage({ [runtimeConfigurationKey]: runtimeConfiguration });
}
});
}
catch (errorThrown) {
errorThrown.sentryExtra = { fetchUrl: url };
error_captureException(errorThrown, {
culprit: 'Querying the extension runtime configuration',
});
}
}
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
void checkRuntimeConfiguration();
}
});
void chrome.alarms.create(ALARM_NAME, { periodInMinutes: 1 });Runtime configuration is fetched every minute
Mixmax requested its runtime config from extension.mixmax.com three times at 60s intervals, updating storage key runtimeConfiguration_production.
Source schedules the same check at startup and via a one-minute alarm, saving changed JSON.
You keep the browser open while the extension is running.
The extension checks Mixmax's runtime-configuration endpoint every minute and stores changed JSON in browser storage.
The runtime-configuration check runs when the background script starts and then repeats once per minute.
| Field | Value | Why it matters | |
|---|---|---|---|
Configuration host | extension.mixmax.com | This is the remote host your browser contacts for extension settings while Mixmax is running. | |
Configuration URL | https://extension.mixmax.com/extension-runtime-config.json?cb=1776498047878 | The full request identifies the runtime-configuration file and includes a changing cache-buster value. | |
Cache-buster value | 1776498047878 | A changing number makes each check a fresh request instead of relying on a cached copy. | |
Stored configuration key | runtimeConfiguration_production | The returned settings are saved locally so the extension can read them later. |
The runtime-configuration fetch, storage update, and one-minute alarm
getExtensionUrl() {
if (this.is(Environment.LOCAL))
return 'https://extension-local.mixmax.com';
else if (this.is(Environment.STAGING))
return 'https://extension-staging.mixmax.com';
else
return 'https://extension.mixmax.com';
}async function checkRuntimeConfiguration() {
if (!navigator.onLine)
return;
// Cache bust the gate URL just in case the browser decides to cache.
const url = Environment.getExtensionUrl() + '/extension-runtime-config.json?cb=' + Date.now();
try {
const response = await fetch(url, { cache: 'no-cache' });
if (!response.ok) {
error_captureMessage('Non-200 response from S3', {
culprit: 'Querying the extension runtime configuration',
extra: {
status: response.status,
body: await response.text().catch((conversionError) => conversionError.message),
},
});
return;
}
const runtimeConfiguration = await response.json();
const runtimeConfigurationKey = `runtimeConfiguration_${Environment.get()}`;
// TODO: use promise API as soon as we're on Mv3
chrome.storage.local.get(runtimeConfigurationKey, (storage) => {
// The API docs for `chrome.storage.local.get` say `storage` should never be null,
// but we've found it sometimes (though rarely) is.
if (!lodash_default().isEqual(runtimeConfiguration, storage === null || storage === void 0 ? void 0 : storage[runtimeConfigurationKey])) {
void setStorage({ [runtimeConfigurationKey]: runtimeConfiguration });
}
});
}
catch (errorThrown) {
errorThrown.sentryExtra = { fetchUrl: url };
error_captureException(errorThrown, {
culprit: 'Querying the extension runtime configuration',
});
}
}
// Listen to `environment` changes to reload the configuration
chrome.storage.local.onChanged.addListener((changes) => {
var _a;
if ((_a = changes.environment) === null || _a === void 0 ? void 0 : _a.newValue) {
// Give time for the Environment to be set
setTimeout(checkRuntimeConfiguration, 0);
}
});
// Check whether configuration has changed now (but give time for the Environment to be set)
//
// NOTE: this may prone to race conditions, but it won't affect production since it is the
// default environment. We can try to revise it when we move to promise-based code after Mv3.
setTimeout(checkRuntimeConfiguration, 0);
// Check back every minute.
const ALARM_NAME = 'runtimeConfiguration';
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
void checkRuntimeConfiguration();
}
});
void chrome.alarms.create(ALARM_NAME, { periodInMinutes: 1 });- extension.mixmax.com
Mixmax extension runtime-configuration host; receives the cache-busted GET and returns JSON that is stored locally.
Error reports send user context to Datadog
We observed Mixmax send Sentry error reports to Datadog after Gmail activated it.
Breadcrumbs included the Gmail address robertfinwitch@gmail.com in loginToken URLs, plus version/channel tags, service tags, stack traces, request history.
You open Gmail while the Mixmax extension is active.
The extension posts Sentry error-reporting envelopes to Datadog that can include your account URL, extension metadata, stack traces, and request history.
| Field | Value | Why it matters | |
|---|---|---|---|
Gmail address in URL | https://app.mixmax.com/api/loginToken?user=robertfinwitch@gmail.com | A breadcrumb URL can identify your account when the email address is embedded in the request path or query string. | |
Extension version and channel | extensionVersion=6.50.1, extensionChannel=main | The report identifies which extension build and release channel produced the event. | |
Stack trace and culprit tag | culprit=Setting cookies for iframes | The report can include details about what code path failed and where the extension grouped the error. | |
API request breadcrumbs | https://app.mixmax.com/api/loginToken?user=robertfinwitch@gmail.com | Request history around the error can reveal what Mixmax API endpoints your browser contacted. |
Sentry setup, scope enrichment, and user email context
function configureSentry(environment) {
init({
dsn: 'https://pub684b6e57211adb32dfdcf62550795cc0@sentry-intake.datadoghq.com/1',
release: chrome.runtime.getManifest().version,
environment,
integrations: [new ExtraErrorData({ depth: 3 })],
async beforeSend(event, { originalException }) {
// If exception is present, check that it's the one that we want to log.
// E.g. we don't want to log network errors in case user has no internet.
if (originalException) {
const shouldLogError = await getShouldLogError(originalException);
if (!shouldLogError) {
return null;
}
}
return event;
},
});
setTags({
extensionVersion: chrome.runtime.getManifest().version,
extensionChannel: Environment.getExtensionChannel(),
// We need to send service name as a tag to Datadog since error tracking group errors by the 'service' tag
service: DD_ERROR_TRACKING_SERVICE,
});
}
chrome.storage.local.onChanged.addListener((changes) => {
var _a;
if ((_a = changes.environment) === null || _a === void 0 ? void 0 : _a.newValue)
configureSentry(changes.environment.newValue);
});
setTimeout(() => configureSentry(Environment.get()), 0);function error_configureScope(scope, { user, tags, extra, fingerprint, culprit } = {}) {
if (user)
scope.setUser(user);
if (tags)
scope.setTags(tags);
if (extra)
scope.setExtras(extra);
if (fingerprint)
scope.setFingerprint(fingerprint);
if (culprit) {
scope.setTag('culprit', culprit);
if (!fingerprint) {
// See https://docs.sentry.io/platforms/node/data-management/event-grouping/sdk-fingerprinting/
scope.setFingerprint(['{{ default }}', culprit]);
}
}
}
/**
* Wrapper for Sentry's captureException.
*/
const error_captureException = function (error, options) {
// Log to console locally and log to Sentry for production.
if (!useRealClient()) {
console.error(error.stack || error, options);
}
else {
withScope((scope) => {
error_configureScope(scope, options);
captureException(error);
});
}
};ExtensionMessageBus.on('setCookiesForIFrames', async ({ user, origin }, _sender, sendResponse) => {
try {
const response = await fetch(`${Environment.getAppUrl()}/api/loginToken?user=${user}`);
if (!response.ok) {
error_captureMessage('Error getting login token', {
culprit: 'Setting cookies for iframes',
extra: { status: response.status },
});
if (sendResponse)
sendResponse(false);
return;
}
const loginToken = await response.json();
if (!loginToken) {
if (sendResponse)
sendResponse(false);
return;
}
chrome.cookies.set({
domain: 'mixmax.com',
expirationDate: Date.now() + 2 * 365 * 24 * 60 * 60,
httpOnly: true,
name: `mixmax_login_token_${user}_${Environment.get()}`,
partitionKey: { topLevelSite: origin },
path: '/',
sameSite: 'no_restriction',
secure: true,
url: 'https://mixmax.com/',
value: loginToken,
}, () => {
if (sendResponse)
sendResponse(true);
});
}
catch (err) {
error_captureException(err, {
user: { email: user },
culprit: 'Setting cookies for iframes',
});
if (sendResponse)
sendResponse(false);
}
});function configureSentry(environment) {
init({
dsn: "https://pub684b6e57211adb32dfdcf62550795cc0@sentry-intake.datadoghq.com/1",
release: chrome.runtime.getManifest().version,
environment,
integrations: [ new ExtraErrorData({
depth: 3
}) ],
async beforeSend(event, {originalException}) {
if (originalException) {
const shouldLogError = await getShouldLogError(originalException);
if (!shouldLogError) {
return null;
}
}
return event;
}
});
setTags({
extensionVersion: chrome.runtime.getManifest().version,
extensionChannel: Environment.getExtensionChannel(),
service: DD_ERROR_TRACKING_SERVICE
});
}
chrome.storage.local.onChanged.addListener((changes => {
var _a;
if ((_a = changes.environment) === null || _a === void 0 ? void 0 : _a.newValue) configureSentry(changes.environment.newValue);
}));
setTimeout((() => configureSentry(Environment.get())), 0);- sentry-intake.datadoghq.com
Datadog Sentry-compatible intake endpoint that receives Mixmax extension session and error-reporting envelopes.