Is YouTube Text Tools - Video Transcript and Summary safe?
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.
Who publishes itm4d.smith - no other listings under this identity
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
You open a YouTube watch page with the extension enabled.
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.
HTTP 200, observed across 5 requests over 3 distinct watch URLs during dynamic analysis.
- Content-Type
- application/json
{ "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" }}- Your device IDe607abd0-fc7b-4de6-8a1d-30e51583cfc4
A UUID created once and stored locally so the vendor can recognize your browser on every future visit.
- Full page URLhttps://www.youtube.com/watch?v=dQw4w9WgXcQ
The complete YouTube watch URL, showing exactly which video you opened.
- Video IDdQw4w9WgXcQ
The YouTube video ID, sent in addition to the full URL.
- Session ID1788898136349
A rotating token that groups events from the same 30-minute browsing session.
- Event nametranscript_loaded
What triggered the event, such as the extension starting or a transcript finishing.
The analytics client that builds and sends every event
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 } } }- 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.