Is Captions for Live Presentations safe?

Medium risk

Captions for Live Presentations sends user IP address, geolocation, and speech transcripts to third-party servers.

On startup, the extension fetches the user's IP address and geolocation from ipapi.co and combines them with device fingerprint data before sending to Google Analytics via the GA4 Measurement Protocol. When the translation feature is enabled, final speech recognition results are transmitted to a developer-operated Google Cloud Run backend over a Socket.IO connection authenticated with a Firebase ID token.

AI Startup Devs LLCv5.3.2.0Chrome Web Store
47Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

IP location added to analytics startup event

MediumunwantedCWE-359Dynamic sandbox

When the extension window opens, its bundle initializes Google Analytics with IP/location data from ipapi.co in analytics properties, plus a client ID and browser/platform/language fields, linking location and device to a persistent ID.

What actually happens

You did this

You open the extension window.

The extension did this

The extension initializes analytics, requests your IP-based location, adds device context, and posts the combined user properties to Google Analytics.

Fields assembled into analytics user properties

  • Persistent analytics ID
    1f7b8a9e-4f1a-4e6a-8b33-9b4e2f4c8c2a

    Lets future analytics events from the extension be tied back to the same browser profile.

  • Your IP address
    203.0.113.42 (illustrative)

    Identifies the network address used for the extension session and supports approximate location lookup.

  • Approximate location
    United States / California / San Francisco / America/Los_Angeles (illustrative)

    Adds city, region, country, and timezone context to the analytics profile.

  • Browser and device context
    Google Chrome 126, Chrome OS, en-US, 1440x900, mobile=false

    Adds details about the browser, platform, language, screen size, and whether the browser appears mobile.

  • Startup event name
    app_initialized

    Marks the analytics upload as part of the extension startup flow.

Captured request

GEThttps://ipapi.co/json/

Captured request

POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-YLS3JCT978&api_secret=<redacted>
Headers
Content-Type
application/json

The code that does this

Analytics initialization path that combines location and device fields

Readable version

Readable analytics class, ipapi lookup, device fields, and GA post

deobfuscated/bundle.js
        var Gd = function() {          function e(t) {            var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {};            Rd(this, e);            var r = e,              s = r.DEFAULT_ENGAGEMENT_TIME_IN_MSEC;            "start_button_click" === t ? r.engagement_start_time = Date.now() : r.engagement_start_time && (s = Date.now() - r.engagement_start_time, "stop_button_click" === t && (r.engagement_start_time = 0)), this.name = t, this.params = Sd(Sd({}, n), {}, {              session_id: Date.now().toString(),              timestamp: (new Date).toISOString(),              engagement_time_msec: s,              debug_mode: !0            }), e.eventData.events.push(this)          }          return Nd(e, [{            key: "postToServer",            value: (n = Od(Cd().mark((function t() {              var n, r;              return Cd().wrap((function(t) {                for (;;) switch (t.prev = t.next) {                  case 0:                    return n = e, t.prev = 1, t.next = 4, fetch("".concat(n.GA_ENDPOINT, "?measurement_id=").concat(n.MEASUREMENT_ID, "&api_secret=").concat(n.API_SECRET), {                      method: "POST",                      headers: {                        "Content-Type": "application/json"                      },                      body: JSON.stringify(n.eventData)                    });                  case 4:                    if ((r = t.sent).ok) {                      t.next = 7;                      break                    }                    throw new Error("GA request failed with status ".concat(r.status));                  case 7:                    n.eventData.events = [], t.next = 15;                    break;                  case 11:                    return t.prev = 11, t.t0 = t.catch(1), console.error("Failed to send event to GA:", t.t0), t.abrupt("return", !1);                  case 15:                  case 16:                  case "end":                    return t.stop()                }              }), t, null, [                [1, 11]              ])            }))), function() {              return n.apply(this, arguments)            })          }], [{            key: "init",            value: (t = Od(Cd().mark((function t() {              var n = this;              return Cd().wrap((function(t) {                for (;;) switch (t.prev = t.next) {                  case 0:                    if (!Id(e, this, np)._) {                      t.next = 2;                      break                    }                    return t.abrupt("return", Id(e, this, np)._);                  case 2:                    np._ = Id(e, this, Promise.all([Id(e, this, Wd).call(this), Id(e, this, Qd).call(this), Id(e, this, Yd).call(this)]).then((function() {                      return Id(e, n, ep).call(n), new e("app_initialized").postToServer(), !0                    })).catch((function(t) {                      throw console.error("Failed to initialize GoogleAnalyticsEvent:", t), np._ = Id(e, n, null), t                    })));                  case 4:                  case "end":                    return t.stop()                }              }), t, this)            }))), function() {              return t.apply(this, arguments)            })          }]);          var t, n        }();        function Wd() {          return Jd.apply(this, arguments)        }        function Jd() {          return (Jd = Od(Cd().mark((function e() {            var t;            return Cd().wrap((function(e) {              for (;;) switch (e.prev = e.next) {                case 0:                  return e.next = 2, chrome.storage.local.get("clientId");                case 2:                  if (t = (t = e.sent).clientId) {                    e.next = 8;                    break                  }                  return t = self.crypto.randomUUID(), e.next = 8, chrome.storage.local.set({                    clientId: t                  });                case 8:                  this.eventData.client_id = t;                case 10:                case "end":                  return e.stop()              }            }), e, this)          })))).apply(this, arguments)        }        function Qd() {          return Xd.apply(this, arguments)        }        function Xd() {          return (Xd = Od(Cd().mark((function e() {            var t, n;            return Cd().wrap((function(e) {              for (;;) switch (e.prev = e.next) {                case 0:                  return e.prev = 0, e.next = 3, fetch("https://ipapi.co/json/");                case 3:                  return t = e.sent, e.next = 6, t.json();                case 6:                  n = e.sent, this.eventData.user_properties = Sd(Sd({}, this.eventData.user_properties), {}, {                    country: {                      value: n.country_name                    },                    region: {                      value: n.region                    },                    city: {                      value: n.city                    },                    timezone: {                      value: n.timezone                    },                    ip: {                      value: n.ip                    }                  }), e.next = 13;                  break;                case 10:                  e.prev = 10, e.t0 = e.catch(0), console.error("failed to retrieve user data");                case 13:                case 14:                case "end":                  return e.stop()              }            }), e, this, [              [0, 10]            ])          })))).apply(this, arguments)        }        function Yd() {          return Zd.apply(this, arguments)        }        function Zd() {          return Zd = Od(Cd().mark((function e() {            var t, n, r;            return Cd().wrap((function(e) {              for (;;) switch (e.prev = e.next) {                case 0:                  return n = function() {                    return n = Od(Cd().mark((function e() {                      return Cd().wrap((function(e) {                        for (;;) switch (e.prev = e.next) {                          case 0:                            return e.abrupt("return", navigator.userAgentData.getHighEntropyValues(["brands"]).then((function(e) {                              var t = e.brands.find((function(e) {                                return !e.brand.includes("Chromium") && !e.brand.includes("Not A")                              }));                              return t ? "".concat(t.brand, " ").concat(t.version) : "Unknown Browser"                            })));                          case 1:                          case "end":                            return e.stop()                        }                      }), e)                    }))), n.apply(this, arguments)                  }, t = function() {                    return n.apply(this, arguments)                  }, e.next = 5, t();                case 5:                  r = e.sent, this.eventData.user_properties = Sd(Sd({}, this.eventData.user_properties), {}, {                    browser: {                      value: r                    },                    platform: {                      value: navigator.userAgentData.platform                    },                    language: {                      value: navigator.language                    },                    screenWidth: {                      value: window.screen.width                    },                    screenHeight: {                      value: window.screen.height                    },                    isMobile: {                      value: /Mobi|Android/i.test(navigator.userAgent)                    }                  });                case 7:                case "end":                  return e.stop()              }            }), e, this)          }))), Zd.apply(this, arguments)        }        function ep() {          return tp.apply(this, arguments)        }        function tp() {          return tp = Od(Cd().mark((function e() {            var t;            return Cd().wrap((function(e) {              for (;;) switch (e.prev = e.next) {                case 0:                  t = function() {                    var e = Od(Cd().mark((function e(t, n) {                      var r, s, i, o, a;                      return Cd().wrap((function(e) {                        for (;;) switch (e.prev = e.next) {                          case 0:                            if (!(Ed.errorHandlingInProgress || null !== (r = n.reason) && void 0 !== r && null !== (r = r.stack) && void 0 !== r && r.includes("GoogleAnalyticsEvent"))) {                              e.next = 2;                              break                            }                            return e.abrupt("return");                          case 2:                            return Ed.errorHandlingInProgress = !0, a = {                              type: t,                              message: (null === (s = n.reason) || void 0 === s ? void 0 : s.message) || n.message || "Unknown error",                              stack: (null === (i = n.reason) || void 0 === i ? void 0 : i.stack) || (null === (o = n.error) || void 0 === o ? void 0 : o.stack) || "No stack available",                              source: n.filename || "N/A",                              line: n.lineno || 0,                              column: n.colno || 0                            }, e.next = 6, new Ed("extension_error", a).postToServer();                          case 6:                            Ed.errorHandlingInProgress = !1;                          case 7:                          case "end":                            return e.stop()                        }                      }), e)                    })));                    return function(t, n) {                      return e.apply(this, arguments)                    }                  }(), addEventListener("unhandledrejection", (function(e) {                    return t("promise_rejection", e)                  })), addEventListener("error", (function(e) {                    return t("runtime_error", e)                  }));                case 3:                case "end":                  return e.stop()              }            }), e)          }))), tp.apply(this, arguments)        }        Ed = Gd, Ad(Gd, "GA_ENDPOINT", "https://www.google-analytics.com/mp/collect"), Ad(Gd, "GA_DEBUG_ENDPOINT", "https://www.google-analytics.com/debug/mp/collect"), Ad(Gd, "DEFAULT_ENGAGEMENT_TIME_IN_MSEC", 0), Ad(Gd, "engagement_start_time", 0), Ad(Gd, "eventData", {          client_id: "",          events: [],          user_properties: {}        }), Ad(Gd, "errorHandlingInProgress", !1);        var np = {          _: null        };        Ad(Gd, "MEASUREMENT_ID", "G-YLS3JCT978"), Ad(Gd, "API_SECRET", "<redacted>");

External services involved in the startup analytics flow

    • ipapi.co

    Receives the location lookup request and returns IP-derived country, region, city, timezone, and IP fields.

    • www.google-analytics.com

    Receives the app_initialized analytics event with client ID, location fields, and browser/device properties.

What it can do

Permissions this extension asks for, as declared in version 2.4.0.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 5.3.2.0, which we have not unpacked yet.

  • Read and change your data on every site you visit

    *://*/* and 1 more

  • Act on the current tab, but only after you click the extension

    activeTab

  • Run its own code inside the pages you visit

    scripting

  • Sign you in with your Google account

    identity

  • See the email address of your Google account

    identity.email

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Captions for Live Presentations 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.

  • www.google-analytics.comwidely used

    Captions for Live Presentations sends data to www.google-analytics.com. A widely used service: 388 other extensions we have analyzed send data here.

  • translate.googleapis.comwidely used

    Captions for Live Presentations sends data to translate.googleapis.com. A widely used service: 92 other extensions we have analyzed send data here.

  • ipapi.co

    Captions for Live Presentations sends data to ipapi.co. 20 other extensions we have analyzed send data here.

  • hello-there-cloudrun-987736763956.us-central1.run.app

    Captions for Live Presentations sends data to hello-there-cloudrun-987736763956.us-central1.run.app. No other extension we have analyzed sends data here.

Updated 2 November 2026ckpgjpenbhnkhcalechemlifbcinneen