Is Tactiq: AI note taker for Google Meet, Zoom and MS Teams safe?
Tactiq is medium risk. After sign-in, observed requests send your email, name, Firebase user ID, and Mixpanel device ID to Mixpanel. One request updates a profile; another links the device ID to the signed-in user. The source also bundles session-recording code.
Who publishes itTactiq HQ Pty Ltd - no other listings under this identity, 1 shared hostname
Tactiq HQ Pty Ltd - no other listings under this identity, 1 shared hostname
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 1 hostname
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Tactiq sends signed-in identity to Mixpanel
After sign-in, observed requests send your email, name, Firebase user ID, and Mixpanel device ID to Mixpanel.
One request updates a profile; another links the device ID to the signed-in user.
The source also bundles session-recording code.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You sign in to Tactiq with Google.
The observed flow reached the Tactiq setup page after the OAuth grant.
The extension posts profile and identify events to Mixpanel.
Those requests contain the email address, full name, Firebase user ID, and Mixpanel device ID.
This local value is the signed-in account ID that the extension then uses in the Mixpanel identity calls.
- Location
- chrome.storage.local key 'v2_user_id'
{ "v2_user_id": "rBg5H0XK7CbCyOHgbaK4NWmsXlf1"}HTTP 200. Mixpanel $set contained $email=lennyyeeman@gmail.com, $name=Lenny Yeeman, $distinct_id/$user_id=rBg5H0XK7CbCyOHgbaK4NWmsXlf1, and a device installation identifier.
HTTP 200. Mixpanel $identify aliased $device:8858dfd6-254f-4a35-a22e-098b9faf5ecd to Firebase UID rBg5H0XK7CbCyOHgbaK4NWmsXlf1.
- Your email addresslennyyeeman@gmail.com
This directly identifies the signed-in account using the extension.
- Your full nameLenny Yeeman
This adds a real-world name to the analytics profile for the signed-in account.
- Your account IDrBg5H0XK7CbCyOHgbaK4NWmsXlf1
This lets the analytics profile connect extension events to the same signed-in account over time.
- Your browser device ID$device:8858dfd6-254f-4a35-a22e-098b9faf5ecd
This links the browser installation that was previously tracked separately to the signed-in account.
- Your account telemetryplan_tier=free; version=3.1.5804; webstore=Chrome; isLoggedIn=true
This describes the extension session and account state attached to the login event.
Service worker initializes Mixpanel and identifies the signed-in user
Deobfuscated Mixpanel initialization and event property builder
background.js function kV() { let e = Be()?.getState().global.installationId; !we() && console.log(`Starting Mixpanel with device ID [${e}]...`), $s.init(we() ? "01ed30ca1f6ac4cd0d4c3f59d96dbbb8" : "9a56981e0851ed25934f43c9cc43dced", { ...e ? { device_id: e } : {} }) } function MV() { return wt() ? void 0 : window } var CV = new Set; function yhe(e, t) { return `${e}::${JSON.stringify(t,Object.keys(t).sort((r,n)=>r.localeCompare(n,void 0,{sensitivity:"base"})))}` } function In(e, t = void 0, r = void 0, n = !1) { xV(e, t); try { if (n) { let i = yhe(e, t ?? {}); if (CV.has(i)) return xV(`Skipping duplicate event: ${e}`, t), !0; CV.add(i) } if (wt()) She(e, t, r); else { let i = hp(), a = typeof t == "object" ? { ...t, platformVersion: i } : { status: t, platformVersion: i }; Yr({ type: "tactiq.trigger-analytic", event: e, properties: a, windowLocation: MV()?.location }) } } catch (i) { console.error(i) } return !0 } function vhe() { return navigator.userAgentData?.brands.find(e => e.brand !== "Chromium" && e.brand.indexOf("Brand") === -1) } function Ew() { return Be()?.getState().user?.id ?? Be()?.getState().global.savedUserId ?? Be()?.getState().global.installationId } function RV(e = {}, t = void 0) { let r = Be()?.getState(), n = t ?? r?.user?.id, i = r?.transcriptions?.currentMeetingJoinTime, a = vhe(), o = i ? Math.floor((Date.now() - (i || 0)) / 1e3) : 0, s = TV(SV(r), vV(r)), u = wV(s), c = { event: "", userId: n, ...n ? { distinct_id: n } : {}, isLoggedIn: !1, widget: !0, version: chrome?.runtime?.getManifest()?.version, webstore: _he(), browserLanguage: MV()?.navigator?.language, browserName: a?.brand, browserVersion: a?.version, duration_seconds: o, plan_tier: u, session_id: Ko(), ...e }; if (!n) { let l = Be()?.getState().global.savedUserId; l ? (c.userId = l, c.hasBeenLoggedOut = !0) : (c.isAnonymous = !0, c.anonymousId = Be()?.getState().global.installationId) } return c.installationId = Be()?.getState().global.installationId, c.isLoggedIn = !c.isAnonymous && !c.hasBeenLoggedOut, c.userId && (c.distinct_id = c.userId), c } async function She(e, t = void 0, r = void 0, n = void 0) { try { await PV(); let i = RV(t, n); i.event = e, r && (qa(r) || $a(r)) && (i.pathnameSHA512 = (0, Fg.sha512)(r.pathname)), r?.host && (i.host = r.host, i.platform = dp(r)); let a = OV(r); a && (i.meetingId = a), $s.track(e, JSON.parse(JSON.stringify(i))) } catch (i) { console.error(i) } }Deobfuscated account identification path
background.js var Pg, _n, DV, Ug = !1, Bg = (e, t) => { _n = e, DV = t, Pg && !Ug && LV().catch(r => console.error(r)) }, LV = async () => { if (!Ug && !(!_n?.uid || !_n.email)) { Ug = !0; try { we() || console.log("Logging into analytics..."), kV(), $s.identify(_n.uid), $s.people.set({ $email: _n.email, $name: _n.displayName }), await Wt.updateContext({ userId: _n.uid, email: _n.email, domain: _n.email.split("@")[1] ?? "anonymous", anonymousId: DV }), await Ng(_n.uid) } catch (e) { Ug = !1, console.error(e) } } }, PV = async () => (Pg || (Pg = (async () => { kV(), _n?.uid && _n.email && await LV() })()), Pg);Deobfuscated external sign-in handler
background.js async function f5(e) { let t = await yc(pr(), e.token); if (t?.user?.uid) Bg(t.user, Be().getState().global.installationId), In("Logged In"); else throw new Error("Could not log in") }- api-js.mixpanel.com
Mixpanel receives the profile update, identify event, and login telemetry from the extension.