Is Tree Style Tab safe?

Medium risk

Tree Style Tab sends user interaction telemetry to Google Analytics, including IP-derived location and a persistent tracking UUID.

Each time the extension is installed, updated, or a user interacts with tab management features, it posts event data to Google Analytics (GA4) with a persistent client ID stored in chrome.storage.local. Before sending, it fetches the user's IP-based location from ipinfo.io (country, city, region, continent) and includes it in every analytics payload. Over 20 distinct interaction types are tracked, covering tab creation, deletion, grouping, workspace save/restore, and search actions.

xingtanzjrv2.3.1Chrome Web Store
45Risk
Who publishes it

xingtanzjr - 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
xingtanzjr

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

Publishers can request a review.

Findings

Interaction telemetry sent with location and a persistent UUID

MediumunwantedCWE-359Dynamic sandbox

The extension sends tab, search, workspace, install and update events to Google Analytics, with a persistent client UUID and a location derived from an ipinfo.io lookup included in each request.

What actually happens

You did this

You use the extension's tab, search, workspace, install, or side-panel controls.

Examples in the shipped side-panel bundle include switching tabs, Google search, drag-and-drop, creating a tab, saving a workspace, restoring a workspace, and deleting a workspace.

The extension did this

The extension sends an analytics event tied to a persistent UUID and IP-derived location.

The analytics sender builds a Google Analytics Measurement Protocol body with client_id, events, params, and user_location when the location lookup succeeds.

Fields the code places in the analytics request

  • Persistent client ID
    9e23d1a4-8d61-4f3a-bc1d-26e1e2d4a9b7

    Lets the same browser installation be linked across multiple extension interactions.

  • Interaction name
    click_switch_tab

    Shows which extension feature you used.

  • Page location (page-view events)
    chrome-extension://oicakdoenlelpjnkoljnaakdofplkgnd/sidepanel.html

    Can identify the extension page or browsing context associated with an analytics event.

  • IP-derived location
    country_id=US, city=San Francisco, region_id=US-CA, continent_id=019, subcontinent_id=021

    Adds city, region, country, continent, and subcontinent context derived from your network address.

  • Session and engagement timing
    session_id=1720794035123, engagement_time_msec=100

    Groups nearby extension activity and records an engagement-time value with the event.

Captured request

GEThttps://ipinfo.io/json

The code expects JSON fields such as country, city, and region, then maps them into Google Analytics location codes.

Captured request

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

The shipped fetch call sends a Google Analytics Measurement Protocol request.

The code that does this

Service worker builds the UUID, location lookup, and analytics POST

Readable version

IP-derived location lookup

service_worker.js
async function getGeoForGA4() {    try {        const result = await chrome.storage.local.get(GEO_CACHE_KEY);        const geoCache = result[GEO_CACHE_KEY];        if (            geoCache &&            geoCache.version === GEO_CACHE_VERSION &&            (Date.now() - geoCache.timestamp) < GEO_CACHE_TTL_MS        ) {            return geoCache.data;        }        const response = await fetch('https://ipinfo.io/json');        if (!response.ok) return null;        const json = await response.json();        if (!json.country) return null;        const data = { country_id: json.country };        if (json.city) {            data.city = json.city;        }        const locationCodes = self.COUNTRY_LOCATION_CODES[json.country];        if (locationCodes) {            data.continent_id = locationCodes.continent_id;            data.subcontinent_id = locationCodes.subcontinent_id;        }        const regionCode = self.REGION_CODES[json.country]?.[json.region];        if (regionCode) {            data.region_id = `${json.country}-${regionCode}`;        }        await chrome.storage.local.set({            [GEO_CACHE_KEY]: {                data,                timestamp: Date.now(),                version: GEO_CACHE_VERSION,            },        });        return data;    } catch (e) {        return null;    }}

Analytics sender

service_worker.js
async function fireGA4Event(name, params = {}) {    try {        let { clientId } = await chrome.storage.local.get('clientId');        if (!clientId) {            clientId = crypto.randomUUID();            await chrome.storage.local.set({ clientId });        }        params.engagement_time_msec = params.engagement_time_msec || '100';        const geo = await getGeoForGA4();        const body = {            client_id: clientId,            events: [{ name, params }],        };        if (geo) {            body.user_location = geo;        }        await fetch(            'https://www.google-analytics.com/mp/collect?measurement_id=G-FZMN8RTLXZ&api_secret=<redacted>',            {                method: 'POST',                body: JSON.stringify(body),            }        );    } catch (e) {        console.error('GA4 event failed', e);    }}

The code that does this

Side-panel bundle sends the same analytics events from tab and workspace handlers

Readable version

Analytics sender from source map

util/analytics.js
async fireEvent(name, params = {}) {  if (!params.session_id) {    params.session_id = await this.getOrCreateSessionId();  }  if (!params.engagement_time_msec) {    params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC;  }  try {    const geo = await this.getGeoLocation();    const body = {      client_id: await this.getOrCreateClientId(),      events: [{ name, params }],    };    if (geo) {      body.user_location = geo;    }    const response = await fetch(      `${this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=<redacted>`,      {        method: 'POST',        body: JSON.stringify(body),      }    );    if (this.debug) {      console.log('[GA4] Request body:', JSON.stringify(body, null, 2));      console.log('[GA4] Response:', await response.text());    }  } catch (e) {    console.error('Google Analytics request failed with an exception', e);  }}

Tab-control event handlers from source map

components/TabTree.jsx
const searchByGoogle = useCallback((query) => {    if (!query || query.trim() === '') return;    const encodedQuery = encodeURIComponent(query);    const url = `https://www.google.com/search?q=${encodedQuery}`;    chrome.tabs.create({ url });    analytics.fireEvent('search_google');}, [chrome.tabs]);const onContainerClick = useCallback((tab) => {    const noTabSelected = !tab || tab.id === -1;        if (noTabSelected) {        searchByGoogle(keyword);    } else if (tab.isBookmark) {        chrome.tabs.create({ url: tab.url });        analytics.fireEvent('click_open_bookmark');    } else if (tab.isGoogleSearch) {        searchByGoogle(tab.title);    } else {        chrome.tabs.update(tab.id, { active: true });        analytics.fireEvent('click_switch_tab');    }    if (window.parent !== window) {        window.parent.postMessage({ type: 'tst-close-overlay' }, '*');    }}, [chrome.tabs, keyword, searchByGoogle]);

Workspace save handler from source map

hooks/useWorkspace.js
const handleSaveWorkspace = useCallback(() => {    const name = wsSaveName.trim();    if (!name) return;    const marks = {};    tabMarks.forEach((value, key) => { marks[key] = value; });    const notes = {};    tabNotes?.forEach((value, key) => { notes[key] = value; });    chrome.runtime.sendMessage({ action: 'saveWorkspace', name, marks, notes }, (resp) => {        if (resp?.success) {            setWsSaving(false);            setWsSaveName('');            setWsSaveStatus('saved');            analytics.fireEvent('save_workspace');            setTimeout(() => setWsSaveStatus(null), 2000);        } else if (resp?.error === 'limit') {            setWsSaving(false);            setWsSaveStatus('limit');            setTimeout(() => setWsSaveStatus(null), 3000);        }    });}, [chrome, tabMarks, tabNotes, wsSaveName]);

External services contacted by the telemetry path

    • ipinfo.io

    Receives a location lookup request so the extension can derive country, city, region, continent, and subcontinent values.

    • www.google-analytics.com

    Receives Google Analytics Measurement Protocol events containing the client UUID, event name, event parameters, and location fields when available.

Where it sends data

Destinations our analysis observed Tree Style Tab 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

    Tree Style Tab sends data to www.google-analytics.com. A widely used service: 388 other extensions we have analyzed send data here.

  • ipinfo.iowidely used

    Tree Style Tab sends data to ipinfo.io. A widely used service: 53 other extensions we have analyzed send data here.

Updated 30 September 2026oicakdoenlelpjnkoljnaakdofplkgnd