Is YouTube Text Tools - Video Transcript and Summary safe?

Medium risk

YouTube Text Tools sends the current YouTube URL, video ID, and stored client/session IDs to its analytics server.

On YouTube watch pages, the content script builds analytics events when the extension starts and when transcripts load. Those events include the page URL, video ID, language/version settings, and client/session identifiers stored in Chrome, then POST them to ytvideotext.summarlyai.com/analytics.

m4d.smithv1.5.16Chrome Web Store
45Risk
Who publishes it

m4d.smith - no other listings under this identity

What this publisher told the store about itself, and the other listings that told it the same thing.

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

Publishers can request a review.

Findings

Every YouTube video you watch is sent to the extension's own analytics server

Opening any YouTube watch page makes the extension send that page's full URL, the video ID, and a persistent device ID to its own analytics server before the transcript loads.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open a YouTube watch page with the extension enabled.

The extension did this

The extension sends the page URL, the video ID, and a persistent device ID to its own analytics server.

This happens automatically before the transcript panel finishes loading, on every watch-page visit.

Captured request
POSThttps://ytvideotext.summarlyai.com/analytics

HTTP 200, observed across 5 requests over 3 distinct watch URLs during dynamic analysis.

Headers
Content-Type
application/json
Body
{  "user_id": "e607abd0-fc7b-4de6-8a1d-30e51583cfc4",  "session_id": "1788898136349",  "context": {    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",    "browser": "Chrome 146.0.0.0",    "device": "desktop",    "os": "macOS 10.15.7"  },  "event_name": "transcript_loaded",  "event_data": {    "videoId": "dQw4w9WgXcQ"  }}
What's in every analytics event
  • Your device ID
    e607abd0-fc7b-4de6-8a1d-30e51583cfc4

    A UUID created once and stored locally so the vendor can recognize your browser on every future visit.

  • Full page URL
    https://www.youtube.com/watch?v=dQw4w9WgXcQ

    The complete YouTube watch URL, showing exactly which video you opened.

  • Video ID
    dQw4w9WgXcQ

    The YouTube video ID, sent in addition to the full URL.

  • Session ID
    1788898136349

    A rotating token that groups events from the same 30-minute browsing session.

  • Event name
    transcript_loaded

    What triggered the event, such as the extension starting or a transcript finishing.

The code that does this

The analytics client that builds and sends every event

Readable version
class Hl {    constructor(e = {}) {      Ht(this, "SESSION_EXPIRATION_IN_MIN", 30);      Ht(this, "ENDPOINT", `${Fl}/analytics`);      let n = chrome.runtime.id;      if (!n) {        n = chrome.runtime.getURL("");        const r = new URL(n);        n = r == null ? void 0 : r.hostname      }      this.extensionId = n, this.onlyExtensionOriginErrors = !0, e.onlyExtensionOriginErrors && (this.onlyExtensionOriginErrors = e.onlyExtensionOriginErrors), this.selfDebugLog = !1, e.selfDebugLog && (this.selfDebugLog = e.selfDebugLog), this.fetchQueue = [], this.isProcessing = !1    }    async getOrCreateClientId() {      var s;      const e = await chrome.storage.local.get(["clientId", "uninstallUrl"]);      let n = e.clientId;      const r = e.uninstallUrl;      n || (n = self.crypto.randomUUID(), await chrome.storage.local.set({        clientId: n      }));      const a = (s = chrome.runtime.getManifest()) == null ? void 0 : s.version;      let i = !r;      if (r && a) try {        const o = new URL(r),          l = o.searchParams.get("uid"),          g = o.searchParams.get("v");        (l !== n || g !== a) && (i = !0)      } catch {        i = !0      }      if (i) try {        chrome.runtime.sendMessage({          type: "UPDATE_UNINSTALL_URL"        })      } catch {}      return n    }    async getOrCreateSessionId() {      let {        sessionData: e      } = await chrome.storage.local.get("sessionData");      const n = Date.now();      return e && e.timestamp && ((n - e.timestamp) / 6e4 > this.SESSION_EXPIRATION_IN_MIN ? e = null : (e.timestamp = n, await chrome.storage.local.set({        sessionData: e      }))), e || (e = {        session_id: n.toString(),        timestamp: n.toString()      }, await chrome.storage.local.set({        sessionData: e      })), e.session_id    }    async getContext() {      const {        browser: e,        os: n,        device: r      } = await this.parseUA();      return {        url: location.href,        browser: e,        device: r,        os: n,        sourceMapId: "89c11be9-7fa4-4576-bf11-484b963e7176"      }    }    async parseUA() {      const n = navigator.userAgent || "";      let r = null,        a = null,        i = "desktop";      return r || (r = this.detectBrowserFromUA(n)), a || (a = this.detectOSFromUA(n)), /^(mobile|tablet|desktop)/i.test(i) || (i = /Tablet|iPad/i.test(n) ? "tablet" : /Mobi|Android(?!.*Tablet)/i.test(n) ? "mobile" : "desktop"), {        browser: r,        os: a,        device: i      }    }    detectBrowserFromUA(e) {      return /Edg\/(\S+)/.test(e) ? `Edge ${RegExp.$1}` : /OPR\/(\S+)/.test(e) ? `Opera ${RegExp.$1}` : /Chrome\/(\S+)/.test(e) && !/Chromium/.test(e) ? `Chrome ${RegExp.$1}` : /Firefox\/(\S+)/.test(e) ? `Firefox ${RegExp.$1}` : /Version\/(\S+).*Safari/.test(e) ? `Safari ${RegExp.$1}` : /Safari\/(\S+)/.test(e) && !/Chrome/.test(e) ? "Safari" : "Unknown"    }    detectOSFromUA(e) {      return /Windows NT 10\.0/.test(e) ? "Windows 10/11" : /Windows NT 6\.3/.test(e) ? "Windows 8.1" : /Windows NT 6\.2/.test(e) ? "Windows 8" : /Windows NT 6\.1/.test(e) ? "Windows 7" : /Mac OS X (\d+[_\.]\d+(?:[_\.]\d+)?)/.test(e) ? `macOS ${RegExp.$1.replace(/_/g,".")}` : /Android ([\d.]+)/.test(e) ? `Android ${RegExp.$1}` : /\(i[^;]+; .*OS (\d+[_\.]\d+(?:[_\.]\d+)?) like Mac OS X/.test(e) ? `iOS ${RegExp.$1.replace(/_/g,".")}` : /Linux/.test(e) ? "Linux" : "Unknown"    }    getMessageFromAny(e) {      return e instanceof Error ? e.message : e && typeof e == "object" && "message" in e ? String(e.message) : String(e)    }    getStackFromAny(e) {      if (e instanceof Error) return e.stack;      if (e && typeof e == "object" && "stack" in e) return String(e.stack)    }    normalizeErrorEvent(e) {      const n = new Date().toISOString();      if (e instanceof Error) return {        type: e.name || "Error",        message: this.getMessageFromAny(e),        stack: this.getStackFromAny(e),        timestamp: n      };      const r = typeof Event < "u" && e instanceof Event;      if (e && typeof e == "object" && !r && ("message" in e || "stack" in e || "type" in e)) return {        type: e.type || e.name || "ErrorObject",        message: this.getMessageFromAny(e),        stack: this.getStackFromAny(e),        timestamp: n      };      const a = {        type: e == null ? void 0 : e.type,        message: "",        stack: void 0,        timestamp: n      };      if (e.type === "unhandledrejection" && "reason" in e) a.message = this.getMessageFromAny(e.reason), a.stack = this.getStackFromAny(e.reason), a.isUnhandledRejection = !0;      else if (e.type === "error")        if ("error" in e) a.filename = e.filename, a.lineno = e.lineno, a.colno = e.colno, e.error ? (a.message = this.getMessageFromAny(e.error), a.stack = this.getStackFromAny(e.error)) : (a.message = e.message || "Script error", a.isCrossOrigin = !0);        else if (e.target && e.target !== window && e.target !== document) {        const i = e.target;        a.message = `Resource error: ${i.tagName}`, a.resourceUrl = i.src || i.href, a.isResourceError = !0      } else a.message = e.message || e.type;      else a.message = e.message || e.type;      return a    }    consoleLog(...e) {      this.selfDebugLog && console.log(...e)    }    async sendError(e, n = {}) {      var r, a;      try {        const i = this.normalizeErrorEvent(e);        if (this.onlyExtensionOriginErrors && this.extensionId && !((r = i.stack) != null && r.includes(`chrome-extension://${this.extensionId}/`)) && !((a = i.filename) != null && a.includes(`chrome-extension://${this.extensionId}/`))) {          this.consoleLog("[analytics] sendError ignoring foreign error:", i);          return        }        this.sendEvent("error", {          ...n,          error: i        })      } catch (i) {        this.consoleLog("[analytics] sendError failed:", i)      }    }    async sendEvent(e, n) {      this.fetchQueue.push({        event_name: e,        event_data: n      }), this.processQueue()    }    async processQueue() {      if (!(this.isProcessing || this.fetchQueue.length === 0)) {        for (this.isProcessing = !0; this.fetchQueue.length > 0;) {          const {            event_name: e,            event_data: n          } = this.fetchQueue.shift();          try {            const r = await this.getContext(),              a = {                user_id: await this.getOrCreateClientId(),                session_id: await this.getOrCreateSessionId(),                context: r,                event_name: e,                event_data: n              };            await fetch(this.ENDPOINT, {              method: "POST",              headers: {                "Content-Type": "application/json"              },              body: JSON.stringify(a)            })          } catch (r) {            this.consoleLog("[analytics] sendEvent failed:", r)          }        }        this.isProcessing = !1      }    }  }
Where this data goes
    • ytvideotext.summarlyai.com

    The extension vendor's own analytics backend; receives the watch URL, video ID, and persistent device ID from every YouTube page you open with the extension enabled.

Where it sends data

Destinations our analysis observed YouTube Text Tools - Video Transcript and Summary 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.

  • ytvideotext.summarlyai.com

    YouTube Text Tools - Video Transcript and Summary sends data to ytvideotext.summarlyai.com. No other extension we have analysed sends data here.

Updated 30 September 2026pcmahconeajhpgleboodnodllkoimcoi