Is Tactiq: AI note taker for Google Meet, Zoom and MS Teams safe?

Medium risk

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.

Tactiq.iov3.1.7049Chrome Web Store
45Risk
Who publishes it

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.

Publisher
Tactiq.io
Declared legal entity
Tactiq HQ Pty Ltd
Registered address
Level 8, 11 York St, Sydney, NSW 2000, AU
Registered contact
Kseniia Svechnikova

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.

app.tactiq.io
Also called by 2 other listings, including TicNote Cloud: AI Meeting Notes — Record & Transcribe with Translation

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
What actually happens
You did this

You sign in to Tactiq with Google.

The observed flow reached the Tactiq setup page after the OAuth grant.

The extension did this

The extension posts profile and identify events to Mixpanel.

Those requests contain the email address, full name, Firebase user ID, and Mixpanel device ID.

What's stored on your device

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'
Contents
{  "v2_user_id": "rBg5H0XK7CbCyOHgbaK4NWmsXlf1"}
Captured request
POSThttps://api-js.mixpanel.com/engage/

HTTP 200. Mixpanel $set contained $email=lennyyeeman@gmail.com, $name=Lenny Yeeman, $distinct_id/$user_id=rBg5H0XK7CbCyOHgbaK4NWmsXlf1, and a device installation identifier.

Captured request
POSThttps://api-js.mixpanel.com/track/

HTTP 200. Mixpanel $identify aliased $device:8858dfd6-254f-4a35-a22e-098b9faf5ecd to Firebase UID rBg5H0XK7CbCyOHgbaK4NWmsXlf1.

Fields observed in the confirmed Mixpanel requests
  • Your email address
    lennyyeeman@gmail.com

    This directly identifies the signed-in account using the extension.

  • Your full name
    Lenny Yeeman

    This adds a real-world name to the analytics profile for the signed-in account.

  • Your account ID
    rBg5H0XK7CbCyOHgbaK4NWmsXlf1

    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 telemetry
    plan_tier=free; version=3.1.5804; webstore=Chrome; isLoggedIn=true

    This describes the extension session and account state attached to the login event.

The code that does this

Service worker initializes Mixpanel and identifies the signed-in user

Readable version

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")  }
External analytics destination reached by the confirmed requests
    • api-js.mixpanel.com

    Mixpanel receives the profile update, identify event, and login telemetry from the extension.

Updated 30 September 2026fggkaccpbmombhnjkjokndojfgagejfb