Is Video Notebook - Visual AI Tutor for Video Learning safe?

Medium risk

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.…

Video Notebookv3.0.26Chrome Web Store
49Risk
Who publishes it

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.

Publisher
Video Notebook
Registered address
226 Yale Rd, Menlo Park, CA 94025-5228, US
Registered contact
Michael Lanza

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

You use Video Notebook features such as notes, saved videos, AI notes, captions, sharing, sign-in, or meetings.

The extension did this

The extension sends feature-use event names to Google Analytics with the same persistent client ID system.

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

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.

Feature-use fields built by the shipped code
  • Analytics client ID
    2ad1bf56-9a12-4e3b-8c44-6e0e77418d10 (illustrative)

    This lets feature-use events be associated with the same browser profile across sessions.

  • Note actions
    Notes-AddText

    These show when note-related features are used.

  • Video actions
    Videos-AddAI

    These show when videos are saved, edited, moved, shared, or added to AI notebooks.

  • AI and caption actions
    Notes-AddAINote

    These show use of AI-generated notes, AI slides, and caption storage or live caption features.

  • Platform context
    platform=youtube; youtubeCategory=Education (illustrative)

    This adds which video platform or YouTube category was active when the event was created.

The code that does this

Feature event definitions and representative call sites

Readable version

Readable content-script GA bridge

default-content.bundle.js
class 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

Readable background GA uploader

background.bundle.js
t.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.js
notes: {  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.js
const 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 = n

Readable 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.js
I.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())
External analytics destination
    • 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
What actually happens
You did this

You open a supported video-site page.

The extension did this

The extension sends a page-view event to Google Analytics with a persistent client ID.

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

Observed during dynamic analysis with a client_id beginning 2ad1bf56 and an events entry containing name 'page_view' and page_path '/'.

Fields sent in the page-view event
  • Analytics client ID
    2ad1bf56-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 name
    page_view

    This marks the request as a page-view event rather than a user-created note or video action.

  • Measurement ID
    G-1RC71516YS

    This identifies the Google Analytics property that receives the event.

The code that does this

Page-view path capture and GA upload code

Readable version

Readable GA request builder

background.bundle.js
t.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.js
class 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 = s

Readable supported-domain page-view trigger

default-content.bundle.js
this.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.js
class 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
External analytics destination
    • www.google-analytics.com

    Receives GA4 Measurement Protocol events containing the extension client ID and event parameters.

Updated 30 September 2026hbklahkfbghjgbclbfcnhpfmajkagnci