Is WhenX: Mark Linkedin Profiles with notes safe?
WhenX sends note text, labels, tags, related URLs, and Google search queries to Mixpanel analytics.
When you create, edit, view, or expand notes, the extension forwards the note body, labels, tags, and associated page URLs through its offscreen Mixpanel analytics script. On Google search result pages, it also reads the current search query and sends it to the same Mixpanel endpoint.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Note text and profile URLs are sent to Mixpanel
When you create, edit, view, or expand a WhenX note, the extension sends the note body, label, tags, and page or profile URL to Mixpanel analytics at api.mixpanel.com, tagged as events like Note Created, Note Edited, and Note Viewed.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You create, edit, view, or expand a note in WhenX.
The note may be attached to a regular page, a search result, or a LinkedIn profile.
The extension routes the note text, label, tags, and associated URL into Mixpanel analytics.
The same analytics path covers created, edited, viewed, and expanded note events.
- Your note textFollow up after interview (illustrative)
Text you write in the note can include private context about a person, company, or task.
- Previous note textIntro call went well (illustrative)
Edits include the earlier text as well as the replacement text, preserving what you changed.
- Note labelfavorite
Labels describe how you categorized the page or profile.
- Note tagsrecruiting, follow-up (illustrative)
Tags add more context about the person, account, or workflow connected to the note.
- Associated page URLhttps://www.linkedin.com/in/jane-doe (illustrative)
The URL ties the note activity to the exact page, search result, or profile where it was used.
- Content-Type
- application/x-www-form-urlencoded
The note analytics route from extension code to Mixpanel
Readable background analytics wrapper and complete analytics object
scripts/background.js function Dd(e, t) { (function() { return Ad.apply(this, arguments) })().then((function() { chrome.runtime.sendMessage({ target: "offscreen-mixpanel", method: e, data: t }) })) } var Od = { init: function() { Dd("init") }, accountCreated: function(e, t, n, r, i) { Dd("accountCreated", { email: e, name: t, avatar: n, fbuid: r, profession: i }) }, login: function(e, t, n, r, i, s, o) { Dd("login", { email: e, name: t, avatar: n, fbuid: r, profession: i, subscriptionType: s, notesCountLocal: o }) }, setPlanType: function(e) { Dd("setPlanType", { subscriptionType: e }) }, redirectedToWebLogin: function() { Dd("redirectedToWebLogin") }, webLoginTokenReturned: function(e) { Dd("webLoginTokenReturned", { userIsNew: e }) }, planUpgraded: function(e) { Dd("planUpgraded", { subscriptionType: e }) }, planDowngraded: function(e) { Dd("planDowngraded", { subscriptionType: e }) }, noteBoxFired: function(e, t) { Dd("noteBoxFired", { method: e, url: t }) }, popupOpened: function(e) { Dd("popupOpened", { url: e }) }, noteCreated: function(e, t, n, r, i, s) { Dd("noteCreated", { noteBody: e, noteSource: t, url: n, label: r, tags: i, notesCount: s }) }, noteEdited: function(e, t, n, r, i, s, o, a) { Dd("noteEdited", { body: e, oldBody: t, label: n, oldLabel: r, tags: i, oldTags: s, url: o, editedFromDashboard: a }) }, noteViewed: function(e, t, n) { Dd("noteViewed", { noteBody: e, url: t, label: n }) }, profileNoteViewed: function(e, t, n, r) { Dd("profileNoteViewed", { noteBody: e, url: t, label: n, tags: r }) }, noteExpanded: function(e, t, n) { Dd("noteExpanded", { noteBody: e, url: t, label: n }) }, profileNoteExpanded: function(e, t, n) { Dd("profileNoteExpanded", { noteBody: e, url: t, label: n }) }, noteBadgeDisplayed: function(e) { Dd("noteBadgeDisplayed", { label: e }) }, noteDeleted: function(e) { Dd("noteDeleted", { url: e }) }, noteCreationFailed: function(e, t, n, r) { Dd("noteCreationFailed", { noteBody: e, url: t, errorMsg: n, version: r }) }, noteEditFailed: function(e, t, n, r, i) { Dd("noteEditFailed", { noteBody: e, url: t, errorMsg: n, version: r, isDashboardNote: i }) }, timestampViewed: function() { Dd("timestampViewed") }, timestampHovered: function() { Dd("timestampHovered") }, profileTimestampViewed: function(e) { Dd("profileTimestampViewed", { url: e }) }, profileTimestampHovered: function(e) { Dd("profileTimestampHovered", { url: e }) }, searchQuery: function(e) { Dd("searchQuery", { query: e }) }, dashboardItemUrlClicked: function(e) { Dd("dashboardItemUrlClicked", { url: e }) }, track: function(e) { Dd("trackEvent", { eventName: e }) }, trackEventWithProperties: function(e, t) { Dd("trackEventWithProperties", { eventName: e, props: t }) }, extensionInstalled: function(e) { Dd("extensionInstalled", { version: e }) }, extensionUpdated: function(e) { Dd("extensionUpdated", { version: e }) }, onboardingContinued: function(e) { Dd("onboardingContinued", { profession: e }) }, onboardingDismissed: function() { Dd("onboardingDismissed") } }; const Rd = Od;Readable offscreen Mixpanel worker
scripts/offscreen-mixpanel.js s().init("3837db1d7db2b8c12bcd394e379bf8c2", { api_host: "https://api.mixpanel.com" }), setInterval(t(i().mark((function e() { return i().wrap((function(e) { for (;;) switch (e.prev = e.next) { case 0: return e.next = 2, navigator.serviceWorker.ready; case 2: e.sent.active.postMessage("keepAlive"); case 3: case "end": return e.stop() } }), e) }))), 2e4), chrome.runtime.onMessage.addListener((function(e, t, r) { if ("offscreen-mixpanel" !== e.target) return !1; var o, i, n, a, c, p, u, _, l, h, d, f, g = e.method, y = e.data; switch (g) { case "accountCreated": _ = y.email, l = y.name, h = y.avatar, d = y.fbuid, f = y.profession, s().alias(_), s().track("Sign Up"), s().people.set({ $email: _, $name: l, $avatar: h, "Firebase uid": d, Profession: f }); break; case "login": ! function(e, t, r, o, i, n, a, c) { s().identify(e), s().register({ "Plan Type": n, "Notes Created Count": a, "Likes Created Count": c }), s().people.set_once({ $email: e, $name: t, $avatar: r, "Firebase uid": o, Profession: i, "Notes Created Count": a, "Likes Created Count": c }), s().people.set({ "Plan Type": n }), s().track("Login") }(y.email, y.name, y.avatar, y.fbuid, y.profession, y.subscriptionType, y.notesCountLocal, y.ratingsCountLocal); break; case "setPlanType": u = y.subscriptionType, s().people.set({ "Plan Type": u }); break; case "redirectedToWebLogin": s().track("Redirected To Web Login"); break; case "webLoginTokenReturned": p = y.userIsNew, s().track("Web Login Token Returned", { "User Is New": p }); break; case "planUpgraded": ! function(e) { s().register({ "Plan Type": e }), s().track("Plan Upgraded"), s().people.set({ "Plan Type": e }) }(y.subscriptionType); break; case "planDowngraded": ! function(e) { s().register({ "Plan Type": e }), s().track("Plan Downgraded"), s().people.set({ "Plan Type": e }) }(y.subscriptionType); break; case "noteBoxFired": ! function(e, t) { s().track("Note Box Opened", { "Note Creation Method": e, "Note Url": t }), s().people.set({ "Last Note Box Fired": (new Date).toISOString() }) }(y.method, y.url); break; case "popupOpened": c = y.url, s().track("Popup Opened", { "Page Url": c }); break; case "noteCreated": ! function(e, t, r, o, i, n) { s().track("Note Created", { "Note Creation Source": t, "Note Label": o, "Note Tags": i, "Note Body": e, "Note Url": r }), n > 2 && s().people.set_once({ "First Note Created": (new Date).toISOString() }); s().people.set({ "Last Note Created": (new Date).toISOString() }), s().people.increment("Notes Created Count") }(y.noteBody, y.noteSource, y.url, y.label, y.tags, y.notesCount); break; case "noteEdited": ! function(e, t, r, o, i, n, a, c) { s().track("Note Edited", { "Note Body": e, "Old Note Body": t, "Note Label": r, "Old Note Label": o, "Note Tags": i, "Old Note Tags": n, "Note Url": a, "Edited From Dashboard": c }), s().people.set({ "Last Note Edited": (new Date).toISOString() }), s().people.increment("Notes Edited Count") }(y.body, y.oldBody, y.label, y.oldLabel, y.tags, y.oldTags, y.url, y.editedFromDashboard); break; case "noteViewed": ! function(e, t, r) { s().track("Note Viewed", { "Note Body": e, "Note Url": t, "Note Label": r }), s().people.set({ "Last Note Viewed": (new Date).toISOString() }), s().people.increment("Notes Viewed Count") }(y.noteBody, y.url, y.label); break; case "profileNoteViewed": ! function(e, t, r, o) { s().track("Profile Note Viewed", { "Note Body": e, "Note Url": t, "Note Label": r, "Note Tags": o }), s().people.set({ "Last Profile Note Viewed": (new Date).toISOString() }), s().people.increment("Profile Notes Viewed Count") }(y.noteBody, y.url, y.label, y.tags); break; case "noteExpanded": ! function(e, t, r) { s().track("Note Expanded", { "Note Body": e, "Note Url": t, "Note Label": r }), s().people.increment("Notes Expanded Count") }(y.noteBody, y.url, y.label); break; case "profileNoteExpanded": ! function(e, t, r) { s().track("Profile Note Expanded", { "Note Body": e, "Note Url": t, "Note Label": r }), s().people.increment("Profile Notes Expanded Count") }(y.noteBody, y.url, y.label); break; case "noteBadgeDisplayed": a = y.label, s().track("Note Badge Displayed", { "Note Label": a }), s().people.increment("Note Badges Displayed Count"); break; case "noteDeleted": ! function(e) { s().track("Note Deleted", { "Note Url": e }) }(y.url); break; case "noteCreationFailed": ! function(e, t, r, o) { s().track("Note Creation Failed", { "Note Body": e, "Note Url": t, "Error Message": r, "Extension Version": o }) }(y.noteBody, y.url, y.errorMsg, y.version); break; case "noteEditFailed": ! function(e, t, r, o, i) { s().track("Note Editing Failed", { "Note Body": e, "Note Url": t, "Error Message": r, "Extension Version": o, "Edited From Dashboard": i }) }(y.noteBody, y.url, y.errorMsg, y.version, y.isDashboardNote); break; case "timestampViewed": s().track("Timestamp Viewed"), s().people.set({ "Last Timestamp Viewed": (new Date).toISOString() }), s().people.increment("Timestamps Viewed Count"); break; case "timestampHovered": s().track("Timestamp Hovered"), s().people.increment("Timestamps Hovered Count"); break; case "profileTimestampViewed": ! function(e) { s().track("Profile Timestamp Viewed", { "Timestamp Url": e }), s().people.set({ "Last Profile Timestamp Viewed": (new Date).toISOString() }), s().people.increment("Profile Timestamps Viewed Count") }(y.url); break; case "profileTimestampHovered": ! function(e) { s().track("Profile Timestamp Hovered", { "Timestamp Url": e }), s().people.increment("Timestamps Hovered Count") }(y.url); break; case "likeViewed": ! function(e) { s().track("Like Viewed", { "Like Url": e }), s().people.set({ "Last Like Viewed": (new Date).toISOString() }), s().people.increment("Likes Viewed Count") }(y.url); break; case "likeCreated": ! function(e) { s().track("Like Created", { "Like Url": e }), s().people.set({ "Last Like Created": (new Date).toISOString() }), s().people.increment("Likes Created Count") }(y.url); break; case "likeDeleted": ! function(e) { s().track("Like Deleted", { "Like Url": e }) }(y.url); break; case "likeCreationFailed": ! function(e, t, r) { s().track("Like Creation Failed", { "Like Url": e, "Error Message": t, "Extension Version": r }) }(y.url, y.errorMsg, y.version); break; case "searchQuery": n = y.query, s().track("Search Query", { "Search Query": n }), s().people.set({ "Last Search": (new Date).toISOString() }), s().people.increment("Searches Count"); break; case "dashboardItemUrlClicked": ! function(e) { s().track("Dashboard Item Url Clicked", { "Dashboard Item Url": e }) }(y.url); break; case "trackEvent": i = y.eventName, s().track(i); break; case "trackEventWithProperties": ! function(e, t) { s().track(e, t) }(y.eventName, y.props); break; case "extensionInstalled": o = y.version, s().track("Install", { "Extension Version": o }), s().people.set({ "Extension Version": o }), s().register({ "Extension Version": o }); break; case "extensionUpdated": ! function(e) { s().track("Extension Updated", { Version: e }), s().people.set({ "Extension Version": e }), s().register({ "Extension Version": e }) }(y.version); break; case "onboardingContinued": ! function(e) { s().people.set({ Profession: e }), s().register({ Profession: e }), s().track("Onboarding Continue Btn Clicked") }(y.profession); break; case "onboardingDismissed": s().track("Onboarding Dismissed") } return "trackEvent" === e.type && function(e) { var t = e.eventName, r = e.properties; r ? s().track(t, r) : s().track(t) }(e), r({ success: !0 }), !0 })) })()})();Readable LinkedIn note UI messages that provide note fields
scripts/createLinkedinNote.js key: "updateNote", value: (t = r(o().mark((function e() { var t; return o().wrap((function(e) { for (;;) switch (e.prev = e.next) { case 0: return t = this, e.abrupt("return", new Promise((function(e, n) { chrome.runtime.sendMessage({ method: "update-note", noteId: t.id, noteBody: t.body, oldNoteBody: t.oldBody, linkUrl: t.url, candidateName: t.candidateName, candidatePic: t.candidatePic, candidatePosition: t.candidatePosition, label: t.label, tags: t.tags, oldLabel: t.oldLabel, oldTags: t.oldTags, isDashboardNote: !1 }, (function(t) { t && t.status && !t.farewell ? e(!0) : e(!1) })) }))); case 2: case "end": return e.stop() } }), e, this) }))), function() { return t.apply(this, arguments) }) }, { key: "addNote", value: (e = r(o().mark((function e() { var t; return o().wrap((function(e) { for (;;) switch (e.prev = e.next) { case 0: return t = this, e.abrupt("return", new Promise((function(e, n) { chrome.runtime.sendMessage({ method: "add-note", noteBody: t.body, linkUrl: t.url, candidateName: t.candidateName, candidatePic: t.candidatePic, candidatePosition: t.candidatePosition, label: t.label, tags: t.tags, src: t.noteSource }, (function(t) { t && t.status && !t.farewell ? e(!0) : e(!1) })) }))); case 3: case "end": return e.stop() }- api.mixpanel.com
Mixpanel analytics endpoint receiving note event properties, including note text fields and associated URLs.
Where it sends data
Destinations our analysis observed WhenX contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- api.mixpanel.comwidely used
WhenX sends data to api.mixpanel.com. A widely used service: 35 other extensions we have analysed send data here.