Is Video Notebook - Visual AI Tutor for Video Learning safe?
Video Notebook is medium risk. Source shows Video Notebook sends note, video, AI, caption, sharing, auth, and meeting events via the GA path used for page views. Traffic confirmed a client ID posted there; event names come from shipped definitions, not a captured body.…
Who publishes itVideo Notebook - no other listings under this identity
Video Notebook - 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
Feature-use events sent to Google Analytics with a client ID
Source shows Video Notebook sends note, video, AI, caption, sharing, auth, and meeting events via the GA path used for page views.
Traffic confirmed a client ID posted there; event names come from shipped definitions, not a captured body.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You use Video Notebook features such as notes, saved videos, AI notes, captions, sharing, sign-in, or meetings.
The extension sends feature-use event names to Google Analytics with the same persistent client ID system.
Observed Google Analytics requests included the persistent client ID with extension-install and page-view events; no feature-use request body was captured in that traffic session.
- Analytics client ID2ad1bf56-9a12-4e3b-8c44-6e0e77418d10 (illustrative)
This lets feature-use events be associated with the same browser profile across sessions.
- Note actionsNotes-AddText
These show when note-related features are used.
- Video actionsVideos-AddAI
These show when videos are saved, edited, moved, shared, or added to AI notebooks.
- AI and caption actionsNotes-AddAINote
These show use of AI-generated notes, AI slides, and caption storage or live caption features.
- Platform contextplatform=youtube; youtubeCategory=Education (illustrative)
This adds which video platform or YouTube category was active when the event was created.
Feature event definitions and representative call sites
Readable content-script GA bridge
default-content.bundle.jsclass a extends n.LocalPulse { events; constructor() { super("GA", {}), this.events = i.gaEvents } async trackEvent(e) { const { platform: t, category: r } = o.NotebookController.getPlatformAndCategory(); t && (e.platform = t), r && (e.youtubeCategory = r), this.sendMessage("trackEvent", e) } async trackPageView() { const e = window.location.pathname + window.location.search; this.sendMessage("trackPageView", e) }}const s = new a;t.GALocalService = sReadable background GA uploader
background.bundle.jst.GAtorChrome = class { events; trackingId; apiSecret; clientId; genericEvents; triggerMap = new Map; endpoint; eventBuffer = []; debounceTimer = null; debounceIntervalMs = 2500; clientIdReady; constructor({ trackingId: e, apiSecret: t, events: r = {}, genericEvents: n = [] }) { this.trackingId = e, this.apiSecret = t, this.clientId = "", this.endpoint = `https://www.google-analytics.com/mp/collect?measurement_id=${this.trackingId}&api_secret=${this.apiSecret}`, this.events = r, this.genericEvents = n, this.buildTriggerMap(n), this.clientIdReady = this.setClientId() } async setClientId() { const e = await n.LocalStorage.get("clientId"); e ? this.clientId = e : (this.clientId = this.generateClientId(), await n.LocalStorage.set("clientId", this.clientId)) } generateClientId() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (e => { const t = 16 * Math.random() | 0; return ("x" === e ? t : 3 & t | 8).toString(16) })) } buildTriggerMap(e) { for (const t of e) for (const e of t.triggers) this.triggerMap.has(e) || this.triggerMap.set(e, []), this.triggerMap.get(e).push(t.event) } trackPageView(e) { const t = e || ("undefined" != typeof window ? window.location.pathname + window.location.search : "/"); this.bufferEvent({ name: "page_view", params: { page_path: t } }) } trackEvent(e) { this.bufferEvent({ name: e.action, params: this.buildParams(e) }); const { category: t, action: r, label: n, ...o } = e, i = this.triggerMap.get(e.action); if (i) for (const e of i) this.bufferEvent({ name: e.action, params: this.buildParams({ ...e, ...o }) }) } buildParams(e) { const { category: t, action: r, label: n, ...o } = e; return { event_category: t, ...n ? { event_label: n } : {}, ...o } } bufferEvent(e) { this.eventBuffer.push(e), this.scheduleFlush() } scheduleFlush() { null !== this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout((() => this.flushEvents()), this.debounceIntervalMs) } async flushEvents() { if (0 === this.eventBuffer.length) return; await this.clientIdReady; const e = [...this.eventBuffer]; this.eventBuffer = [], this.debounceTimer = null; const t = { client_id: this.clientId, events: e }; this.sendPayload(t) } sendPayload(e) { fetch(this.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(e) }).then((e => { e.ok || console.error("GA tracking error:", e.statusText) })).catch((e => { console.error("GA tracking error:", e) })) }}Readable event-name definitions
background.bundle.jsnotes: { add: () => ({ category: "Notes", action: "Notes-Add" }), addText: () => ({ category: "Notes", action: "Notes-AddText" }), addImage: () => ({ category: "Notes", action: "Notes-AddImage" }), addBookmark: () => ({ category: "Notes", action: "Notes-AddBookmark" }), addAINote: () => ({ category: "Notes", action: "Notes-AddAINote" }), addAISlide: () => ({ category: "Notes", action: "Notes-AddAISlide" }), edit: () => ({ category: "Notes", action: "Notes-Edit" }), delete: () => ({ category: "Notes", action: "Notes-Delete" }), timestampClick: () => ({ category: "Notes", action: "Notes-TimestampClick" }), scroll: () => ({ category: "Notes", action: "Notes-Scroll" })},captions: { addStored: () => ({ category: "Captions", action: "Captions-AddStored" }), addLive: () => ({ category: "Captions", action: "Captions-AddLive" })},videos: { add: () => ({ category: "Videos", action: "Videos-Add" }), addAI: () => ({ category: "Videos", action: "Videos-AddAI" }), edit: () => ({ category: "Videos", action: "Videos-Edit" }), move: () => ({ category: "Videos", action: "Videos-Move" }), moveToAI: () => ({ category: "Videos", action: "Videos-MoveToAI" }), share: () => ({ category: "Videos", action: "Videos-Share" }), quickAdd: () => ({ category: "Videos", action: "Videos-QuickAdd" })}Readable generic event triggers
background.bundle.jsconst n = [{ event: r.general.total(), triggers: ["Extension-Loaded"]}, { event: r.general.active(), triggers: ["Extension-Open", "Links-OpenWebApp", "Links-OpenHelpNotebook", "Notes-Add", "Notes-Edit", "Notes-Delete", "Notes-TimestampClick", "Notes-Scroll", "Captions-AddStored", "Captions-AddLive", "Videos-Add", "Videos-AddAI", "Videos-Edit", "Videos-Move", "Videos-MoveToAI", "Videos-Share", "Videos-QuickAdd", "Notebooks-Add", "VideoPlayer-Play", "AIContent-Enable", "AIContent-Disable", "AIContent-SettingEnable", "AIContent-SettingDisable", "AI-AskAssistant", "Auth-SignIn", "Auth-SignInGoogle", "Share-CopyLink", "Share-SendEmail"]}, { event: r.general.consumers(), triggers: ["Links-OpenWebApp", "Notes-TimestampClick", "Notes-Scroll", "Videos-Share", "VideoPlayer-Play"]}, { event: r.general.producers(), triggers: ["Notes-AddText", "Notes-AddImage", "Notes-AddBookmark", "Notes-AddAINote", "Notes-AddAISlide", "Notes-Edit", "Notes-Delete", "Videos-Add", "Videos-AddAI", "Videos-Edit", "Videos-Move", "Videos-MoveToAI", "Videos-QuickAdd"]}, { event: r.general.producersAI(), triggers: ["Notes-AddAINote", "Notes-AddAISlide", "Videos-AddAI", "Videos-MoveToAI"]}, { event: r.general.producersPersonal(), triggers: ["Notes-AddText", "Notes-AddImage", "Notes-AddBookmark", "Notes-Edit"]}, { event: r.notes.add(), triggers: ["Notes-AddText", "Notes-AddImage", "Notes-AddBookmark", "Notes-AddAINote", "Notes-AddAISlide"]}];t.genericGAEvents = nReadable text-note call site
default-content.bundle.js(0, l.addTextNote)(o).then((e => { D({ ...e, saved: !0 }), g.GALocalService.trackEvent(g.GALocalService.events.notes.addText())})), m({ ...o, saved: !1, editable: !1, noteType: a.NoteType.Text}, !0)} else { const n = (0, i.v4)(), o = (0, u.calculateStartTimeSeconds)(e.startTime, t.startTime); d.NotesBufferLocalService.addNote({ id: n, noteType: a.NoteType.Text, startTime: e.startTime, startTimeSeconds: o, textContent: r.textContent, htmlContent: r.htmlContent, createdAt: (new Date).toISOString() }), m({ id: n, noteType: a.NoteType.Text, startTime: e.startTime, startTimeSeconds: o, textContent: r.textContent, htmlContent: r.htmlContent, video: t, buffered: !0, saved: !1, editable: !1, user: { isOwned: !0 } }, !0), g.GALocalService.trackEvent(g.GALocalService.events.notes.addText()), h?.()}Readable video/caption call site
default-content.bundle.jsI.GALocalService.trackEvent(I.GALocalService.events.videos.add()), m?.notebookType === D.PublicNotebookType.AI && I.GALocalService.trackEvent(I.GALocalService.events.videos.addAI()), (d.length || b) && I.GALocalService.trackEvent(I.GALocalService.events.captions.addStored())- www.google-analytics.com
Receives GA4 Measurement Protocol events containing the extension client ID and event parameters.
Video page paths sent to Google Analytics with a persistent ID
We observed Video Notebook send a Google Analytics request when a supported video page loaded, carrying a stored client ID and a page_view event for the path, linking repeat views to one identifier.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open a supported video-site page.
The extension sends a page-view event to Google Analytics with a persistent client ID.
Observed during dynamic analysis with a client_id beginning 2ad1bf56 and an events entry containing name 'page_view' and page_path '/'.
- Analytics client ID2ad1bf56-9a12-4e3b-8c44-6e0e77418d10 (illustrative)
This lets multiple extension events be associated with the same browser profile over time.
- Current page path/watch?v=dQw4w9WgXcQ
This shows which supported video page you were viewing, including the query string when one is present.
- Event namepage_view
This marks the request as a page-view event rather than a user-created note or video action.
- Measurement IDG-1RC71516YS
This identifies the Google Analytics property that receives the event.
Page-view path capture and GA upload code
Readable GA request builder
background.bundle.jst.GAtorChrome = class { events; trackingId; apiSecret; clientId; genericEvents; triggerMap = new Map; endpoint; eventBuffer = []; debounceTimer = null; debounceIntervalMs = 2500; clientIdReady; constructor({ trackingId: e, apiSecret: t, events: r = {}, genericEvents: n = [] }) { this.trackingId = e, this.apiSecret = t, this.clientId = "", this.endpoint = `https://www.google-analytics.com/mp/collect?measurement_id=${this.trackingId}&api_secret=${this.apiSecret}`, this.events = r, this.genericEvents = n, this.buildTriggerMap(n), this.clientIdReady = this.setClientId() } async setClientId() { const e = await n.LocalStorage.get("clientId"); e ? this.clientId = e : (this.clientId = this.generateClientId(), await n.LocalStorage.set("clientId", this.clientId)) } generateClientId() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (e => { const t = 16 * Math.random() | 0; return ("x" === e ? t : 3 & t | 8).toString(16) })) } buildTriggerMap(e) { for (const t of e) for (const e of t.triggers) this.triggerMap.has(e) || this.triggerMap.set(e, []), this.triggerMap.get(e).push(t.event) } trackPageView(e) { const t = e || ("undefined" != typeof window ? window.location.pathname + window.location.search : "/"); this.bufferEvent({ name: "page_view", params: { page_path: t } }) } trackEvent(e) { this.bufferEvent({ name: e.action, params: this.buildParams(e) }); const { category: t, action: r, label: n, ...o } = e, i = this.triggerMap.get(e.action); if (i) for (const e of i) this.bufferEvent({ name: e.action, params: this.buildParams({ ...e, ...o }) }) } buildParams(e) { const { category: t, action: r, label: n, ...o } = e; return { event_category: t, ...n ? { event_label: n } : {}, ...o } } bufferEvent(e) { this.eventBuffer.push(e), this.scheduleFlush() } scheduleFlush() { null !== this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout((() => this.flushEvents()), this.debounceIntervalMs) } async flushEvents() { if (0 === this.eventBuffer.length) return; await this.clientIdReady; const e = [...this.eventBuffer]; this.eventBuffer = [], this.debounceTimer = null; const t = { client_id: this.clientId, events: e }; this.sendPayload(t) } sendPayload(e) { fetch(this.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(e) }).then((e => { e.ok || console.error("GA tracking error:", e.statusText) })).catch((e => { console.error("GA tracking error:", e) })) }}Readable GA service initialization
background.bundle.jsclass a extends o.GlobalPulse { gator; events; constructor() { super("GA", {}), this.gator = new n.GAtorChrome({ trackingId: "G-1RC71516YS", apiSecret: "<redacted>", events: i.gaEvents, genericEvents: i.genericGAEvents }), this.setEvents({ trackEvent: this.trackEvent.bind(this), trackPageView: this.trackPageView.bind(this) }), this.events = i.gaEvents } async trackEvent(e) { this.gator.trackEvent(e) } async trackPageView(e) { this.gator.trackPageView(e) }}const s = new a;t.GAGlobalService = sReadable supported-domain page-view trigger
default-content.bundle.jsthis.domainStatus = t, (0, d.vnButtonLog)("bootstrap", "checkDomainStatus_ok", { domainStatus: t, frameId: n?.frameId, url: n?.url}), t === u.DomainStatus.White ? (window.contentScriptMgr = new i.ContentScriptMgr, window.videoMgr = new s.default(window.contentScriptMgr), o.GALocalService.trackPageView(), (0, d.vnButtonLog)("bootstrap", "managers_started_white", {})) : (0, d.vnButtonLog)("bootstrap", "managers_skipped_not_white", { domainStatus: t})Readable content-script GA bridge
default-content.bundle.jsclass a extends n.LocalPulse { events; constructor() { super("GA", {}), this.events = i.gaEvents } async trackEvent(e) { const { platform: t, category: r } = o.NotebookController.getPlatformAndCategory(); t && (e.platform = t), r && (e.youtubeCategory = r), this.sendMessage("trackEvent", e) } async trackPageView() { const e = window.location.pathname + window.location.search; this.sendMessage("trackPageView", e) }}const s = new a;t.GALocalService = s- www.google-analytics.com
Receives GA4 Measurement Protocol events containing the extension client ID and event parameters.