Is Record Google Meet safe?

Medium risk

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.

dansolo2025v2.0.2Chrome Web Store
45Risk

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

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 did this

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.

Fields assembled into the GA4 event body
  • Your analytics client ID
    8b4f3e2a-15d0-4a4d-9c9a-1d9f4c9b6f32

    This links separate extension events from the same browser profile over time.

  • Your analytics session
    1720794625123

    This groups multiple actions from the same use session.

  • Your country
    US

    This adds location context to the analytics event.

  • Your subscription state
    free

    This tells the analytics endpoint whether the browser profile is treated as free or paid.

  • Your account state
    authenticated: true; recordings: 7

    This says whether the extension considers you signed in and may include a recording count from the stored profile.

  • Your browser version
    Chrome 126

    This adds environment details to the event so actions can be segmented by browser version.

  • Your recording settings
    camera: true; microphone: true; type: screen; duration: 185 seconds

    Recording-start and recording-stop events add details such as camera, microphone, recording type, and duration.

Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-ZM6PMQ5DVM&api_secret=<redacted>

No GA4 collection request was observed during dynamic analysis; this endpoint and method are from the shipped source code.

Headers
Content-Type
application/json
The code that does this

The shipped code that constructs and posts the GA4 event

Readable version

Readable analytics sender and field enrichment

background.bundle.js
function 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.js
chrome.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)  }}());
Event names and the user actions that feed the same sender
  1. same handler

    You did

    You open the extension from the toolbar.

    The extension did

    The extension queues an extension-opened analytics event.

  2. same workflow

    You did

    You start a recording.

    The extension did

    The extension queues a recording-started event with camera, microphone, and recording-type fields.

  3. same workflow

    You did

    You stop a recording.

    The extension did

    The extension queues a recording-stopped event with the recording duration.

  4. same workflow

    You did

    You save a recording to Drive.

    The extension did

    The extension queues a saved-to-Drive analytics event.

External services involved in this analytics path
    • 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 caveat

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.

Updated 30 September 2026bepmaejckfhbdbikphclmnmclpiheche