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

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceSTORAGE DUMP
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.

Locationchrome.storage.local key 'v2_user_id'
Contents (JSON)
{
  "v2_user_id": "rBg5H0XK7CbCyOHgbaK4NWmsXlf1"
}
03EvidenceNETWORK CAPTURE
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.
04EvidenceNETWORK CAPTURE
Captured request
POSThttps://api-js.mixpanel.com/track/
HTTP 200. Mixpanel $identify aliased $device:8858dfd6-254f-4a35-a22e-098b9faf5ecd to Firebase UID rBg5H0XK7CbCyOHgbaK4NWmsXlf1.
05EvidenceFIELD TABLE
Fields observed in the confirmed Mixpanel requests
FieldValueWhy it matters
Your email address
lennyyeeman@gmail.comThis directly identifies the signed-in account using the extension.
Your full name
Lenny YeemanThis adds a real-world name to the analytics profile for the signed-in account.
Your account ID
rBg5H0XK7CbCyOHgbaK4NWmsXlf1This lets the analytics profile connect extension events to the same signed-in account over time.
Your browser device ID
$device:8858dfd6-254f-4a35-a22e-098b9faf5ecdThis 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=trueThis describes the extension session and account state attached to the login event.
06EvidenceCODE COMPARE
The code that does this

Service worker initializes Mixpanel and identifies the signed-in user

What it actually does
Deobfuscated Mixpanel initialization and event property builderbackground.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 pathbackground.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 handlerbackground.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")
  }
07EvidenceTHIRD PARTY LIST
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