Is Voice Control for ChatGPT safe?

Medium risk

Voice Control for ChatGPT sends ChatGPT conversation URLs and page titles to Google Analytics with a persistent user identifier.

Each time you navigate within ChatGPT, the extension captures the page title and conversation URL (which contains a unique conversation GUID) and transmits them to Google Analytics via the GA4 Measurement Protocol. A persistent client ID generated at install is attached to every event, allowing all your ChatGPT browsing activity to be linked across sessions.

Theis Frøhlichv4.4.2Chrome Web Store
45Risk
Who publishes it

Aidia ApS - 1 other listing from the same operator, none carrying a finding

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

Publisher
Theis Frøhlich
Declared legal entity
Aidia ApS
Registered address
Nøddehaven 4, Værløse 3500, DK
Registered contact
Theis Frøhlich

Same store account

1 other listing published from this account, 3k+ users between them, none of them carrying a finding.

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

ChatGPT Conversation URLs Sent to Google Analytics

Each time you open or switch ChatGPT conversations, this extension sends the URL and page title to Google Analytics.

Dynamic analysis confirmed repeated requests, each carrying a persistent install ID, linking your visits over time.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open or navigate between conversations on ChatGPT.

This includes loading chatgpt.com, opening a new chat, or switching to an existing conversation.

The extension did this

The extension sends the conversation URL and page title to Google Analytics, along with a persistent identifier unique to your browser installation.

A session identifier that groups navigations within one browsing period is also included.

02EvidenceFIELD TABLE
Data fields included in every analytics request
FieldValueWhy it matters
Persistent installation ID
6df0f8d6-147d-4b59-a138-1e5c83e730bfA unique ID generated at install, kept permanently. Every analytics request carries it, linking your ChatGPT visits across sessions.
Session ID
1719530400000A timestamp-based value that groups your navigations within one browsing period. Resets after 30 minutes of inactivity.
Conversation URL
https://chatgpt.com/c/7a3f9c21-1d4e-4b8a-9f2d-3e8c5b6d1a04The full address of your ChatGPT conversation, including its unique ID. The URL path reveals which conversation you opened.
Page title
How to configure AWS S3 bucket policies — ChatGPTThe title of the ChatGPT page at the time of navigation, which often reflects the topic or opening message of a conversation.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-67EVHBE3DC&api_secret=<redacted>
204 No Content
Headers
Content-Typetext/plain;charset=UTF-8
Body
{
  "client_id": "6df0f8d6-147d-4b59-a138-1e5c83e730bf",
  "events": [
    [
      {
        "name": "page_view",
        "params": {
          "page_title": "How to configure AWS S3 bucket policies — ChatGPT",
          "page_location": "https://chatgpt.com/c/7a3f9c21-1d4e-4b8a-9f2d-3e8c5b6d1a04",
          "client_id": "6df0f8d6-147d-4b59-a138-1e5c83e730bf",
          "session_id": "1719530400000",
          "engagement_time_msec": 100
        }
      }
    ]
  ]
}
04EvidenceCODE COMPARE
The code that does this

Background worker: persistent client ID and GA4 Measurement Protocol dispatch

What it actually does
Client ID generation and storagesrc/pages/background/index.js
// On first run, generate a UUID and persist it in local storage.
// All subsequent calls return the same UUID, creating a durable cross-session identity.
async function getOrCreateClientId() {
  let clientId = (await chrome.storage.local.get('clientId')).clientId;
  if (!clientId) {
    clientId = self.crypto.randomUUID();
    await chrome.storage.local.set({ clientId });
  }
  return clientId;
}
Session ID management (30-minute TTL)src/pages/background/index.js
const SESSION_TIMEOUT_MINUTES = 30;

async function getOrCreateSessionId() {
  let { sessionData } = await chrome.storage.session.get('sessionData');
  const now = Date.now();
  if (sessionData?.timestamp) {
    const minutesElapsed = (now - sessionData.timestamp) / 60000;
    if (minutesElapsed > SESSION_TIMEOUT_MINUTES) {
      sessionData = null; // session expired, create new one
    } else {
      sessionData.timestamp = now; // refresh session TTL
      await chrome.storage.session.set({ sessionData });
    }
  }
  if (!sessionData) {
    sessionData = { session_id: now.toString(), timestamp: now.toString() };
    await chrome.storage.session.set({ sessionData });
  }
  return sessionData.session_id;
}
GA4 endpoint constants and event flushsrc/pages/background/index.js
const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
const MEASUREMENT_ID = 'G-67EVHBE3DC';
const API_SECRET = '<redacted>'; // GA4 Measurement Protocol api_secret — value redacted
const ENGAGEMENT_TIME_MS = 100;

let eventQueue = [];
let lastFlushTime = Date.now();

async function flushEventQueue() {
  if (eventQueue.length === 0) return;
  lastFlushTime = Date.now();
  const events = eventQueue; // capture current queue
  eventQueue = [];           // reset before async work
  const clientId = await getOrCreateClientId();
  await fetch(
    `${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
    {
      method: 'POST',
      body: JSON.stringify({ client_id: clientId, events: [events] }),
    }
  );
}
Event enrichment and queue dispatchsrc/pages/background/index.js
// Queue an event, flushing immediately or after a 2-second debounce.
function queueEvent(event) {
  eventQueue.push(event);
  if (eventQueue.length > 0) {
    if (lastFlushTime + 2000 > Date.now()) {
      flushEventQueue(); // flush immediately if recent
    } else {
      setTimeout(flushEventQueue, 2000); // debounce
    }
  }
}

// Enrich an incoming gaEvent message from the content script with tracking IDs.
async function sendGaEvent(event) {
  const clientId = await getOrCreateClientId();
  const sessionId = await getOrCreateSessionId();
  const params = event?.params ?? {};
  const enrichedEvent = {
    ...event,
    params: {
      ...params,
      client_id: clientId,
      session_id: sessionId,
      engagement_time_msec: ENGAGEMENT_TIME_MS,
    },
  };
  queueEvent(enrichedEvent);
}

// Message listener: handles gaEvent messages relayed by the content script
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  // ... other message types ...
  if (message.gaEvent) await sendGaEvent(message.gaEvent);
  // ...
  sendResponse({ message: 'Message received' });
});
05EvidenceTHIRD PARTY LIST
External host receiving analytics data
  • www.google-analytics.com

    Google Analytics Measurement Protocol (GA4) endpoint. Receives page_view events with ChatGPT URLs, titles, and a persistent install UUID on every navigation. Operated by Google.

What it can do

Permissions this extension asks for, as declared in version 4.3.1. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 4.4.2, which we have not unpacked yet.

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Voice Control for ChatGPT 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

    Voice Control for ChatGPT sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.

Updated 30 September 2026eollffkcakegifhacjnlnegohfdlidhn