Is Tree Style Tab safe?
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.
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
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.
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 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.
| Field | Value | Why it matters | |
|---|---|---|---|
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. |
Service worker builds the UUID, location lookup, and analytics POST
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;
}
}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);
}
}Side-panel bundle sends the same analytics events from tab and workspace handlers
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);
}
}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]);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]);- 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.