Is Shopify Theme Detector by Shopify Spy safe?

Medium risk

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.

Extposev1.0.0Chrome Web Store
47Risk
Who publishes it

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.

Publisher
Extpose

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
What actually happens
You did this

The extension background worker starts after installation or browser startup.

The extension did this

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.

Fields constructed for the GA4 request
  • Browser profile identifier
    550e8400-e29b-41d4-a716-446655440000 (illustrative UUID)

    Lets repeated telemetry events be tied back to the same browser profile over time.

  • Event name
    run

    Shows that the extension is reporting a recurring run event rather than a page action you took.

  • Session identifier
    1720796235123 (illustrative timestamp-based value)

    Groups events from the same extension session for analytics reporting.

  • Engagement time
    100

    Adds a fixed analytics timing value to the event payload.

Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-8L0K6710QG&api_secret=<redacted>
When this fires

Every 1 hour

The extension sends the run event once when analytics starts, then repeats it every 60 minutes.

The code that does this

Analytics helper and startup call in the shipped extension

Readable version

Readable analytics module

js/background.js
23: [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.js
24: [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}]
External host that receives the telemetry
    • 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.

Updated 30 September 2026iahafehbldmcbobiiffbplmlckgafdhj