Is Captions for Live Presentations safe?
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-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
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 open the extension window.
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 ID1f7b8a9e-4f1a-4e6a-8b33-9b4e2f4c8c2a
Lets future analytics events from the extension be tied back to the same browser profile.
- Your IP address203.0.113.42 (illustrative)
Identifies the network address used for the extension session and supports approximate location lookup.
- Approximate locationUnited States / California / San Francisco / America/Los_Angeles (illustrative)
Adds city, region, country, and timezone context to the analytics profile.
- Browser and device contextGoogle 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 nameapp_initialized
Marks the analytics upload as part of the extension startup flow.
Captured request
Captured request
- Content-Type
- application/json
The code that does this
Analytics initialization path that combines location and device fields
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.
