Is Dupe.com: Find similar products for less safe?
Dupe.com sends the URL and product metadata from every HTTPS page you visit to Google Analytics.
The extension's content script runs on all HTTPS pages and reports each page view to Google Analytics, including the full URL, product title, price, currency, and image URL. Events are batched in the background service worker and POSTed to Google Analytics using a hardcoded API secret. A separate postMessage handler allows any web page to toggle the extension's side panel overlay before an origin check is enforced.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Product pages you view, plus your account ID, go to a second tracking backend
Separate from its analytics, Dupe.com's worker builds an event per product view (URL, price, brand, category, seller, SKU, rating, session ID, account/temp ID), sent to Tinybird with a per-install token behind a flag on in our Amazon test.
What actually happens
You open a product-detail page on a shopping site, for example, an Amazon or eBay listing, while the extension is installed and enabled.
This fires from background page-visit tracking; you don't need to open the extension's popup or interact with it.
The worker builds an event with the product's URL, price, brand, and category, tags it with your account or temp ID and a session ID, and sends it to Tinybird.
The behavior is gated behind a server-controlled feature flag; in dynamic testing the flag was enabled and the event fired exactly as described here.
Fields included in each product-page-view event sent to Tinybird
- The product page addresshttps://www.amazon.com/Apple-MacBook-13-inch-256GB-Storage/dp/B0863TXGM3
The full URL of the product page you viewed, with common ad-tracking parameters (utm_source, gclid) stripped but the rest intact.
- Price, currency, brand & category164 GBP, Apple, Laptops
The product's listed price and currency, plus its brand and category as detected from the page.
- Rating and review count4.6 stars, 62,639 reviews
The product's star rating and total review count as shown on the page.
- Selected size, color, seller & SKUSize: 13-inch, Color: Space Gray, Seller: Amazon.com, SKU: B0863TXGM3
When you've picked a specific size, color, or seller listing, that selection is captured along with the item's SKU.
- Session IDs_1783561654549 (illustrative)
An identifier that groups all your page views from a single browsing session together.
- Your Dupe account ID or temporary IDa1b2c3d4-e5f6-4a1b-9c3d-7e8f9a0b1c2d (illustrative)
If signed in, your account ID is attached; if not, a persistent temp ID. Either way your product views stay linkable to one identity.
Captured request
HTTP 202 Accepted, Tinybird's Events API queued the request for ingestion.
- Content-Type
- application/x-ndjson
- Authorization
- Bearer <redacted>
{ "event_type": "pdp_view", "shop_key": "amazon.com", "url": "https://www.amazon.com/Apple-MacBook-13-inch-256GB-Storage/dp/B0863TXGM3", "price": 164, "currency": "GBP", "category": "Laptops", "rating": 4.6, "review_count": 62639, "timestamp": "2026-07-06T03:55:02.118Z", "session_id": "<redacted>", "user_id": "<redacted>", "temp_user_id": "<redacted>", "extension_version": "0.3.0", "browser": "chrome"}The code that does this
The EventEmitter that batches and sends product-page events, and the code paths that feed it
Reformatted for readability — variable names are unchanged; this build is minified, not identifier-obfuscated
background.jsconst TINYBIRD_ENDPOINT = "https://api.us-east.tinybird.co/v0/events?name=extension_events", TINYBIRD_TOKEN = "<redacted>", // hardcoded bearer token, identical for every install FEATURE_FLAG_KEY = "extension-tinybird-events", FLUSH_INTERVAL_MS = 1e4, // 10 seconds FLUSH_BATCH_SIZE = 20, BUFFER_STORAGE_KEY = "dupe:event-buffer", MAX_BUFFERED = 500;class EventEmitter { constructor(token) { this.queue = []; this.enabled = false; this.flushTimer = null; this.token = token || TINYBIRD_TOKEN; } async init() { if (!this.token) return; try { this.enabled = !!(await FeatureFlags.getFeatureFlag(FEATURE_FLAG_KEY, 9e5)); // 900s cache, remote-controlled } catch { this.enabled = false; } if (!this.enabled) return; // no-op entirely when the remote flag is off await this.restoreBuffer(); this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS); } async track(event) { if (!this.enabled) return; const enveloped = await this.envelope(event); this.queue.push(enveloped); if (this.queue.length >= FLUSH_BATCH_SIZE) this.flush(); } async flush() { if (this.queue.length === 0) return; const batch = this.queue.splice(0); const ndjsonBody = batch.map(e => JSON.stringify(e)).join("\n"); try { const res = await fetch(TINYBIRD_ENDPOINT, { method: "POST", headers: { Authorization: `Bearer ${this.token}`, "Content-Type": "application/x-ndjson" }, body: ndjsonBody }); if (!res.ok) throw new Error(`TinyBird HTTP ${res.status}`); } catch { await this.bufferEvents(batch); } } async envelope(event) { const session = (await chrome.storage.session.get("dupe:shopping-session"))["dupe:shopping-session"]; const sessionId = session?.startedAt ? `s_${session.startedAt}` : this.fallbackSessionId; const user = await Auth.getActiveUser(); const tempUserId = await Auth.getTempUserId(); return { ...event, timestamp: new Date().toISOString(), session_id: sessionId, user_id: user?.uid || null, // your signed-in Dupe account ID, when logged in temp_user_id: tempUserId, // persistent pseudo-anonymous ID otherwise extension_version: chrome.runtime.getManifest().version, browser: "chrome" }; }}const tinybirdEmitter = new EventEmitter();Where product-page data enters the tracker, reformatted for readability
background.js// Fired from the product-detail-page detector on every PDP view:tinybirdEmitter.track({ event_type: "pdp_view", shop_key: shopKey, url: stripTrackingParams(pageUrl), image_url: metadata?.imageUrl, title: metadata?.title, price: metadata?.price, currency: metadata?.currency, brand: metadata?.brand, category: metadata?.category, selected_color: metadata?.selectedColor, selected_size: metadata?.selectedSize, gender: metadata?.gender, seller: metadata?.seller, sku: metadata?.sku, material: metadata?.material, shipping: metadata?.shipping, returns: metadata?.returns, warranty: metadata?.warranty, has_color: Array.isArray(metadata?.colors) && metadata.colors.length > 0 ? 1 : 0, has_size: Array.isArray(metadata?.sizes) && metadata.sizes.length > 0 ? 1 : 0, has_specs: metadata?.specs ? 1 : 0, specs_json: metadata?.specs ? JSON.stringify(metadata.specs) : null, has_rating: metadata?.rating != null ? 1 : 0, rating: metadata?.rating, review_count: metadata?.reviewCount, review_summary: metadata?.reviewSummary, review_snippets_json: Array.isArray(metadata?.reviewSnippets) && metadata.reviewSnippets.length > 0 ? JSON.stringify(metadata.reviewSnippets.slice(0, 5)) : null});// Internal message handler — any component in the extension can trigger// a Tinybird event by sending a TRACK_TINYBIRD message, with no field// validation applied before it's enveloped and queued:onMessage(Messages.TRACK_TINYBIRD, async ({ data }) => { tinybirdEmitter.track(data); return { status: "ok" };});When this fires
Every 10 seconds
Once the remote feature flag is enabled, queued events are flushed to Tinybird every 10 seconds on a timer, or immediately if 20 events queue up before the timer fires.
Where your product-page data is sent
- api.us-east.tinybird.co
Tinybird's Events API. Receives batched product-page events (URL, price, brand, category) tagged with your Dupe account or temp ID, under a token hardcoded in the extension.
Every page you visit, shopping or not, is reported to Google Analytics
Dupe.com's content script runs on every HTTPS page, reading the full URL plus product metadata found.
The background worker batches three views, sends them to Analytics under a persistent install ID.
News visits trigger the same report.
What actually happens
You open any HTTPS webpage while the extension is installed and enabled, a news article, a work tool, anything.
Nothing shopping-related has to happen; simply loading the page is enough to trigger tracking.
A content script running on that page reads the full page address and any product-style metadata it can find, then reports the visit to Google Analytics.
The report goes out even when the page is not a product page and even when you never interact with the extension's UI.
Fields included in every page-view report sent to Google Analytics
- The full page addresshttps://www.npr.org/2026/07/01/politics/some-article
Every page you load is captured verbatim, the full URL, whether or not it's a shopping site.
- Site identifier (derived from the URL)npr.org
A per-domain key computed from the page address, letting Dupe group all your visits to that domain together.
- Page titleBreaking News, Latest Updates | NPR
Whatever the page's <title> or Open Graph title is, even on non-shopping pages.
- Price, currency & image (if found)49.99 USD, https://cdn.shop.example/img/sku-4821.jpg
On e-commerce pages, the price, currency, and main image are read from the page's Open Graph tags and sent alongside the URL.
- Your persistent analytics ID550e8400-e29b-41d4-a716-446655440000
A stable ID tied to your install, letting every page view be linked back to the same identity across browsing sessions.
When this fires
When a batch threshold is hit · flushes at 3 queued events
Page views aren't sent to Google one at a time. They're queued in local browser storage and the extension automatically flushes the whole queue to Google Analytics as soon as 3 page views have accumulated.
Captured request
HTTP 204 No Content, Google Analytics accepted the batch. The Measurement Protocol returns an empty success response for writes.
{ "client_id": "<persistent-client-id>", "user_id": "anonymous", "events": [ { "name": "extension_page_viewed", "params": { "medium": "extension", "shopKey": "bbc.com", "url": "https://www.bbc.com/news", "isPdp": false, "standDown": false } }, { "name": "extension_page_viewed", "params": { "medium": "extension", "shopKey": "theguardian.com", "url": "https://www.theguardian.com/us", "isPdp": false, "standDown": false } }, { "name": "extension_page_viewed", "params": { "medium": "extension", "shopKey": "npr.org", "url": "https://www.npr.org", "isPdp": false, "standDown": false } } ]}The code that does this
The content-script tracker and the background handler that ship every page view to Google Analytics
Reformatted for readability — variable names are unchanged; this build is minified, not identifier-obfuscated
content-scripts/open-tab-parse.jsconst { isPdp: i, pageChanged: r } = pv(), n = async s => { const c = `https://dupe.com/${s}`; await yi(vi.OPEN_TAB, { url: c }) };return Ra.useEffect(() => { typeof i > "u" || (async () => { const s = of(window.location.href), c = await Zh(), // extractMetadata(): title, price, currency, imageUrl, brand, description... d = !!ov.get("stand-down"); Y0.track(Wt.ExtensionPageViewed, { medium: "extension", shopKey: s, // per-domain key derived from the URL url: window.location.href, // the FULL current page address isPdp: i, // true only on a detected product page metadata: c, // OG title/price/currency/image when present standDown: d }, { skipPosthog: !0 }) // routed to GA only, not to PostHog })()}, [i, r]), Ra.useEffect(() => { // ...separate listener for the "dupe this" context-menu action, omitted...}, []), nullReformatted for readability, with the hardcoded GA identifiers pulled out as named constants
background.jsconst EVENT_BATCH_KEY = "dupe:eventBatchViews", BATCH_SIZE = 3, GA_API_SECRET = "<redacted>", GA_MEASUREMENT_ID = "G-CZFTQBRF2Q", GA_ENDPOINT = "https://www.google-analytics.com/mp/collect", normalizeName = e => e.toLowerCase().replace(/\s/g, "_"), loadBatch = async () => await storage.get(EVENT_BATCH_KEY) || [], saveBatch = async e => { await storage.set(EVENT_BATCH_KEY, e) }, clearBatch = async () => await storage.remove(EVENT_BATCH_KEY), postToGA = async (userId, events, clientId = "") => await fetch( `${GA_ENDPOINT}?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`, { method: "POST", body: JSON.stringify({ client_id: clientId || userId, user_id: userId, events: events.map(n => ({ name: normalizeName(n.name), params: { ...n.params } })) }) } );function registerGaTrackHandler() { onMessage(GA_TRACK, async ({ data: e }) => { const { name: t, userId: r, properties: n, clientId: s } = e; try { const params = { ...n }; const event = { userId: r || "anonymous", name: t, params }; if (t === Events.ExtensionPageViewed) { const batch = await loadBatch() || []; batch.push(event); if (batch.length > 100) batch.splice(0, batch.length - 100); await saveBatch(batch); if (batch.length >= BATCH_SIZE) { // flush once 3 page views have queued const res = await postToGA(r || "", batch, s); if (res.ok) await clearBatch(); return res.statusText || {}; } return {}; } return (await postToGA(r || "", [event], s)).statusText || {}; } catch (err) { return { status: "error", statusMessage: "An error occurred while trying to track the event" }; } })}Where your page-view data is sent
- www.google-analytics.com
Google's GA4 endpoint. Receives batched page-view events, URL, page title and product metadata, tagged with a persistent client ID, under Dupe.com's own Analytics property.
Where it sends data
Destinations our analysis observed Dupe.com 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
Dupe.com sends data to www.google-analytics.com. A widely used service: 388 other extensions we have analyzed send data here.