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.2.1Chrome Web Store
45Risk

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

Interaction telemetry sent with location and a persistent UUID

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.

01EvidenceCAUSE EFFECT
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.

02EvidenceFIELD TABLE
Fields the code places in the analytics request
FieldValueWhy it matters
Persistent client ID
9e23d1a4-8d61-4f3a-bc1d-26e1e2d4a9b7Lets the same browser installation be linked across multiple extension interactions.
Interaction name
click_switch_tabShows which extension feature you used.
Page location (page-view events)
chrome-extension://oicakdoenlelpjnkoljnaakdofplkgnd/sidepanel.htmlCan 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=021Adds city, region, country, continent, and subcontinent context derived from your network address.
Session and engagement timing
session_id=1720794035123, engagement_time_msec=100Groups nearby extension activity and records an engagement-time value with the event.
03EvidenceNETWORK CAPTURE
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.
04EvidenceNETWORK CAPTURE
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.
05EvidenceCODE COMPARE
The code that does this

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

What it actually does
IP-derived location lookupservice_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 senderservice_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);
    }
}
06EvidenceCODE COMPARE
The code that does this

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

What it actually does
Analytics sender from source maputil/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 mapcomponents/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 maphooks/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]);
07EvidenceTHIRD PARTY LIST
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.

Data recipients

www.google-analytics.comipinfo.io
Updated 17 September 2026oicakdoenlelpjnkoljnaakdofplkgnd