Is Record Google Meet safe?
Record Google Meet sends your Google account email and IP address to app.easymeetrecord.com for subscription and geo-based paywall checks.
When you use the extension, it fetches your public IP from api.ipify.org and forwards it—along with your account status and first-access date—to app.easymeetrecord.com to determine whether to show a paywall. It also retrieves your Google account email and profile via OAuth and sends them to the same backend on every recording interaction to verify subscription status. Additionally, detailed usage events including recording duration, camera/mic state, and locale are sent to Google Analytics with a persistent client ID.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
GA4 events include user and subscription attributes
Record Google Meet sends GA4 events for opening, recording start/stop, and Drive saves, each with a client ID, session ID, country, and status.
Dynamic analysis saw it active but not this, needing popup interaction or a real recording.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open the extension or use its recording workflow.
The cited call sites include extension open, first open, recording start, recording stop, saving to Drive, and recording-limit events.
The extension builds an analytics event that includes a persistent browser-profile identifier and account-related attributes.
The source code posts that event to the Google Analytics Measurement Protocol endpoint.
- Your analytics client ID8b4f3e2a-15d0-4a4d-9c9a-1d9f4c9b6f32
This links separate extension events from the same browser profile over time.
- Your analytics session1720794625123
This groups multiple actions from the same use session.
- Your countryUS
This adds location context to the analytics event.
- Your subscription statefree
This tells the analytics endpoint whether the browser profile is treated as free or paid.
- Your account stateauthenticated: true; recordings: 7
This says whether the extension considers you signed in and may include a recording count from the stored profile.
- Your browser versionChrome 126
This adds environment details to the event so actions can be segmented by browser version.
- Your recording settingscamera: true; microphone: true; type: screen; duration: 185 seconds
Recording-start and recording-stop events add details such as camera, microphone, recording type, and duration.
No GA4 collection request was observed during dynamic analysis; this endpoint and method are from the shipped source code.
- Content-Type
- application/json
The shipped code that constructs and posts the GA4 event
Readable analytics sender and field enrichment
background.bundle.jsfunction Xr(e) { return Hr.apply(this, arguments)}function Hr() { return Hr = Bt(Pt().mark(function e(t) { var r, n, o, a, c = arguments; return Pt().wrap(function(e) { for (;;) switch (e.prev = e.next) { case 0: return r = c.length > 1 && void 0 !== c[1] ? c[1] : {}, e.next = 3, $r(); case 3: return n = e.sent, e.next = 6, en(); case 6: return o = e.sent, e.next = 9, Qr(r); case 9: return r.engagement_time_msec = 100, r.session_id = o, a = { client_id: n, events: [{ name: t, params: r }] }, e.prev = 13, e.next = 16, fetch("https://www.google-analytics.com/mp/collect?measurement_id=G-ZM6PMQ5DVM&api_secret=<redacted>", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(a) }); case 16: e.next = 21; break; case 18: e.prev = 18, e.t0 = e.catch(13), console.error("Failed to send GA4 event:", e.t0); case 21: case "end": return e.stop() } }, e, null, [ [13, 18] ]) })), Hr.apply(this, arguments)}function Qr(e) { return Zr.apply(this, arguments)}function Zr() { return (Zr = Bt(Pt().mark(function e(t) { var r, n, o, a, c, i, u, s; return Pt().wrap(function(e) { for (;;) switch (e.prev = e.next) { case 0: return e.prev = 0, e.next = 3, chrome.storage.local.get(["supabaseAuth"]); case 3: return n = e.sent, o = n.supabaseAuth, e.next = 7, chrome.storage.local.get(["subscriptionStatus"]); case 7: return a = e.sent, c = a.subscriptionStatus, i = "unknown", e.prev = 10, e.next = 13, me.getUserCountry(); case 13: u = e.sent, i = u || "unknown", e.next = 20; break; case 17: e.prev = 17, e.t0 = e.catch(10), console.warn("Failed to get user country for analytics:", e.t0); case 20: return t.user_country = i, t.subscription_status = c || "free", t.locale = chrome.i18n.getMessage("@@ui_locale") || "en", e.prev = 23, e.next = 26, me.isOldUser(); case 26: s = e.sent, t.user_type = s ? "old" : "new", e.next = 33; break; case 30: e.prev = 30, e.t1 = e.catch(23), t.user_type = "unknown"; case 33: o && o.user ? (t.user_authenticated = !0, o.profile && (t.number_of_recordings = o.profile.number_of_recordings || 0, o.profile.subscription_status && (t.subscription_status = o.profile.subscription_status))) : t.user_authenticated = !1, t.browser_version = navigator.userAgent.includes("Chrome/") && parseInt(null === (r = navigator.userAgent.match(/Chrome\/([0-9]+)/)) || void 0 === r ? void 0 : r[1]) || "unknown", t.event_timestamp = Date.now(), e.next = 45; break; case 38: e.prev = 38, e.t2 = e.catch(0), console.warn("Failed to enrich event with user data:", e.t2), t.user_country = "unknown", t.subscription_status = "free", t.user_authenticated = !1, t.locale = "en"; case 45: case "end": return e.stop() } }, e, null, [ [0, 38], [10, 17], [23, 30] ]) }))).apply(this, arguments)}function $r() { return new Promise(function(e) { chrome.storage.local.get(["clientId"], function(t) { if (t.clientId) e(t.clientId); else { var r = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(e) { var t = 16 * Math.random() | 0; return ("x" === e ? t : 3 & t | 8).toString(16) }); chrome.storage.local.set({ clientId: r }, function() { e(r) }) } }) })}Readable extension-open trigger
background.bundle.jschrome.runtime.onStartup.addListener(function() {}), chrome.action.onClicked.addListener(function() { var e = Bt(Pt().mark(function e(t) { var r, n, o, a, c; return Pt().wrap(function(e) { for (;;) switch (e.prev = e.next) { case 0: return e.next = 2, chrome.storage.local.get(["recording"]); case 2: if (r = e.sent, !r.recording) { e.next = 14; break } return or(), fr({ type: "stop-recording-tab" }), e.next = 9, chrome.storage.local.get(["activeTab"]); case 9: n = e.sent, o = n.activeTab, chrome.tabs.get(o, function(e) { e ? mt(o, { type: "stop-recording-tab" }) : (mt(t.id, { type: "stop-recording-tab" }), chrome.storage.local.set({ activeTab: t.id })) }), e.next = 18; break; case 14: if (t && t.url) { e.next = 17; break } return chrome.tabs.create({ url: "playground.html", active: !0 }), e.abrupt("return"); case 17: !1 === navigator.onLine && !t.url.includes("/playground.html") && !t.url.includes("/setup.html") || t.url.startsWith("chrome://") || t.url.startsWith("chrome-extension://") && !t.url.includes("/playground.html") && !t.url.includes("/setup.html") || t.url.includes("stackoverflow.com/") || t.url.includes("chrome.google.com/webstore") || t.url.includes("chromewebstore.google.com") ? (chrome.tabs.create({ url: "playground.html", active: !0 }).then(function(e) { chrome.storage.local.set({ activeTab: e.id }) }), Xr("extension_opened", { locale: chrome.i18n.getMessage("@@ui_locale"), inPopup: !1 })) : (mt(t.id, { type: "toggle-popup" }), chrome.storage.local.set({ activeTab: t.id }), Xr("extension_opened", { locale: chrome.i18n.getMessage("@@ui_locale"), inPopup: !0 })); case 18: return e.next = 20, chrome.storage.local.get(["firstTime"]); case 20: if (a = e.sent, !(a.firstTime && t && t.url && t.url.includes(chrome.runtime.getURL("setup.html")))) { e.next = 29; break } return Xr("first_open"), chrome.storage.local.set({ firstTime: !1 }), e.next = 27, gt(); case 27: (c = e.sent) && c.id && mt(c.id, { type: "setup-complete" }); case 29: case "end": return e.stop() } }, e) })); return function(t) { return e.apply(this, arguments) }}());same handler
You didYou open the extension from the toolbar.
The extension didThe extension queues an extension-opened analytics event.
same workflow
You didYou start a recording.
The extension didThe extension queues a recording-started event with camera, microphone, and recording-type fields.
same workflow
You didYou stop a recording.
The extension didThe extension queues a recording-stopped event with the recording duration.
same workflow
You didYou save a recording to Drive.
The extension didThe extension queues a saved-to-Drive analytics event.
- www.google-analytics.com
Receives the Measurement Protocol event built by Xr().
- api.ipify.org
Returns the browser's public IP address for the extension's country lookup path.
- app.easymeetrecord.com
Receives the geo-check request and returns location data used by analytics enrichment.
Dynamic analysis observed other extension traffic, including the geo-check path, but did not observe a request to the GA4 collection endpoint. The prior observation recorded that the GA4 events require popup interaction or a real recording lifecycle trigger, so this claim is supported by static proof of the shipped code path rather than by a captured GA4 request body.
Where it sends data
Destinations our analysis observed Record Google Meet 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
Record Google Meet sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.
- www.googleapis.comwidely used
Record Google Meet sends data to www.googleapis.com. A widely used service: 44 other extensions we have analysed send data here.
- api.ipify.orgwidely used
Record Google Meet sends data to api.ipify.org. A widely used service: 41 other extensions we have analysed send data here.
- app.easymeetrecord.com
Record Google Meet sends data to app.easymeetrecord.com. No other extension we have analysed sends data here.