Is Shopify Theme Detector by Shopify Spy safe?
Shopify Theme Detector sends periodic telemetry to Google Analytics and probes every website the user visits for Shopify store metadata.
On startup and every 60 minutes, the extension posts a 'run' event to Google Analytics using a persistent UUID stored in local extension storage. On every tab navigation, the extension fetches a /meta.json file from the visited site's origin to detect Shopify store details such as store name, currency, and location. Results are cached locally for 30 days.
Who publishes itExtpose - 11 other listings from the same operator, 1 of them carrying a finding
Extpose - 11 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
11 other listings published from this account, 2.0M+ users between them. 1 of them carries a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Persistent GA4 UUID telemetry sent hourly
The extension starts GA4 telemetry when its background worker initializes and schedules the same event every 60 minutes, storing a UUID as cid and sending it as the GA4 client_id, tying install and hourly events to one profile.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
The extension background worker starts after installation or browser startup.
The extension sends a run event to Google Analytics and schedules the same event every hour.
The event includes a UUID that is generated once and kept in extension local storage.
- Browser profile identifier550e8400-e29b-41d4-a716-446655440000 (illustrative UUID)
Lets repeated telemetry events be tied back to the same browser profile over time.
- Event namerun
Shows that the extension is reporting a recurring run event rather than a page action you took.
- Session identifier1720796235123 (illustrative timestamp-based value)
Groups events from the same extension session for analytics reporting.
- Engagement time100
Adds a fixed analytics timing value to the event payload.
Every 1 hour
The extension sends the run event once when analytics starts, then repeats it every 60 minutes.
Analytics helper and startup call in the shipped extension
Readable analytics module
js/background.js23: [function(rx, ZA, Tb) { "use strict"; Object.defineProperty(Tb, "__esModule", { value: true }), Tb.analytics = Tb.SESSION_EXPIRATION_IN_MIN = Tb.DEFAULT_ENGAGEMENT_TIME_MSEC = Tb.CLIENT_ID_PROP_NAME = Tb.GA_DEBUG_ENDPOINT = Tb.GA_ENDPOINT = void 0; const wB = rx("uuid"); Tb.GA_ENDPOINT = "https://www.google-analytics.com/mp/collect", Tb.GA_DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect", Tb.CLIENT_ID_PROP_NAME = "cid", Tb.DEFAULT_ENGAGEMENT_TIME_MSEC = 100, Tb.SESSION_EXPIRATION_IN_MIN = 30; class lo { constructor(rx, ZA, Tb = false) { this.measurement_id = rx, this.api_secret = ZA, this.debug = Tb } async getOrCreateClientId() { const rx = await chrome.storage.local.get(Tb.CLIENT_ID_PROP_NAME); let ZA = rx[Tb.CLIENT_ID_PROP_NAME]; if (!ZA) ZA = (0, wB.v4)(), await chrome.storage.local.set({ [Tb.CLIENT_ID_PROP_NAME]: ZA }); return ZA } async getOrCreateSessionId() { let { sessionData: rx } = await chrome.storage.session.get("sessionData"); const ZA = Date.now(); if (rx && rx.timestamp) { const wB = (ZA - rx.timestamp) / 6e4; if (wB > Tb.SESSION_EXPIRATION_IN_MIN) rx = null; else rx.timestamp = ZA, await chrome.storage.session.set({ sessionData: rx }) } if (!rx) rx = { session_id: ZA.toString(), timestamp: ZA.toString() }, await chrome.storage.session.set({ sessionData: rx }); return rx.session_id } async fireEvent(rx, ZA = {}) { if (!ZA.session_id) ZA.session_id = await this.getOrCreateSessionId(); if (!ZA.engagement_time_msec) ZA.engagement_time_msec = Tb.DEFAULT_ENGAGEMENT_TIME_MSEC; try { const wB = await fetch(`${this.debug?Tb.GA_DEBUG_ENDPOINT:Tb.GA_ENDPOINT}?measurement_id=${this.measurement_id}&api_secret=${this.api_secret}`, { method: "POST", body: JSON.stringify({ client_id: await this.getOrCreateClientId(), events: [{ name: rx, params: ZA }] }) }); if (!this.debug) return } catch (rx) {} } async firePageViewEvent(rx, ZA, Tb = {}) { return this.fireEvent("page_view", Object.assign({ page_title: rx, page_location: ZA }, Tb)) } async fireErrorEvent(rx, ZA = {}) { return this.fireEvent("extension_error", Object.assign(Object.assign({}, rx), ZA)) } } const zC = (rx, ZA) => { const Tb = new lo(rx, ZA); Tb.fireEvent("run"), chrome.alarms.create(rx, { periodInMinutes: 60 }), chrome.alarms.onAlarm.addListener((() => { Tb.fireEvent("run") })) }; Tb.analytics = zC}, { uuid: 2}]Readable startup module
js/background.js24: [function(rx, ZA, Tb) { "use strict"; Object.defineProperty(Tb, "__esModule", { value: true }); const wB = rx("./common"), lo = rx("./utils/store_meta"), zC = "This website is not powered by Shopify", uA = async rx => { const ZA = await (0, lo.getSummary)(rx); if (!ZA) return await chrome.action.setPopup({ tabId: rx.id, popup: "" }), await chrome.action.setTitle({ tabId: rx.id, title: zC }), await chrome.action.setIcon({ tabId: rx.id, path: { 16: chrome.runtime.getURL("icons/icon_disabled16.png"), 32: chrome.runtime.getURL("icons/icon_disabled32.png"), 48: chrome.runtime.getURL("icons/icon_disabled48.png"), 64: chrome.runtime.getURL("icons/icon_disabled64.png"), 128: chrome.runtime.getURL("icons/icon_disabled128.png"), 300: chrome.runtime.getURL("icons/icon_disabled300.png") } }), void await chrome.action.disable(rx.id); const Tb = [`Domain: ${ZA.domain}`, `Store: ${ZA.name}`, `Location: ${ZA.city}` + (ZA.province ? `, ${ZA.province}` : "") + (ZA.country ? `, ${ZA.country}` : ""), `Currency: ${ZA.currency?ZA.currency:"N/A"}`]; await chrome.action.setPopup({ tabId: rx.id, popup: chrome.runtime.getURL("popup.html") }), await chrome.action.setTitle({ tabId: rx.id, title: Tb.join("\n") }), await chrome.action.setIcon({ tabId: rx.id, path: { 16: chrome.runtime.getURL("icons/icon16.png"), 32: chrome.runtime.getURL("icons/icon32.png"), 48: chrome.runtime.getURL("icons/icon48.png"), 64: chrome.runtime.getURL("icons/icon64.png"), 128: chrome.runtime.getURL("icons/icon128.png"), 300: chrome.runtime.getURL("icons/icon300.png") } }), await chrome.action.enable(rx.id) }, XH = async () => { const rx = await chrome.tabs.query({}); for (const ZA of rx) await uA(ZA); chrome.tabs.onUpdated.addListener(((rx, ZA, Tb) => uA(Tb))) }; XH(), (0, wB.analytics)("G-8L0K6710QG", "<redacted>")}, { "./common": 25, "./utils/store_meta": 28}]- www.google-analytics.com
Google Analytics Measurement Protocol endpoint receiving the extension's run event, persistent client ID, session ID, and engagement timing value.
What it can do
Permissions this extension asks for, as declared in version 1.0.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Schedule its own background tasks
alarms
Store data in your browser
storage
Where it sends data
Destinations our analysis observed Shopify Theme Detector 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
Shopify Theme Detector sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.