UNPKG

@topsort/toppie-sdk

Version:

Toppie sdk is a JS library that allows to integrate Topsort auctions and analytics into your website.

880 lines (879 loc) 24 kB
class N { constructor() { this.debug = !1, this.baseURL = "https://gtm.topsort.workers.dev/", this.cookieName = "tsuid", this.deviceType = typeof window < "u" && window.navigator.userAgent.includes("Mobile") ? "mobile" : "desktop", this.enabledCatalogIngestion = !1; } setAppId(e) { this.appId = e; } getAppId() { return this.appId; } setBaseURL(e) { this.baseURL = e; } setDebug(e) { this.debug = e; } setCookieName(e) { this.cookieName = e; } setUserId(e) { this.userId = e; } setEnabledCatalogIngestion(e) { this.enabledCatalogIngestion = e; } } const I = new N(); function A() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (e) => { const n = Math.random() * 16 | 0; return (e === "x" ? n : n & 3 | 8).toString(16); }); } function q() { var n; const r = I.cookieName; return (n = new RegExp(`(^|;)\\s*${r}\\s*=\\s*([^;]+)`).exec(document.cookie)) == null ? void 0 : n.pop(); } function B(r) { const e = I.cookieName; document.cookie = `${e}=${r};max-age=31536000`; } function x() { if (I.userId) return I.userId; const r = q(); if (r) return r; const e = A(); return B(e), e; } const L = /* @__PURE__ */ new Map(); function O(r, e = void 0) { L.set(r, e); } function $(r) { return L.has(r); } const S = (r, { method: e, body: n, params: t }) => { let s = `${I.baseURL}${r}`; const c = { method: e, body: n, headers: { "Content-Type": "application/json", "X-TS-ID": `${I.appId}` } }; return t && (s += `?${new URLSearchParams(t).toString()}`), window.fetch(s, c); }; class H { constructor() { this.service_url = I.baseURL, this.deviceType = I.deviceType, this.user_id = x(); } setUserId(e) { this.user_id = e; } send(e) { return S("v1/events", { method: "POST", body: JSON.stringify({ ...e }) }); } event(e, n, t = !1) { if ("resolvedBidId" in n && !t) { const s = n.resolvedBidId; if ($(`${s}-${e}`)) return; O(`${s}-${e}`, e); } return this.send({ [e]: [ { id: A(), opaqueUserId: this.user_id, occurredAt: (/* @__PURE__ */ new Date()).toISOString(), deviceType: this.deviceType, placement: this.placement(), ...n } ] }); } placement() { let e = window.location.pathname; const n = new URLSearchParams(window.location.search); return n.delete("address"), n.toString() && (e += `?${n.toString()}`), { path: e }; } } const b = new H(); async function _(r) { try { const e = await S("v1/auctions", { method: "POST", body: JSON.stringify(r) }); return e.ok ? await e.json() : void 0; } catch { return; } } const P = { position: "append" }; function C(r, e, n = P) { n.position === "append" ? e.appendChild(r) : e.insertBefore(r, e.firstChild); } function D({ id: r, className: e, slotId: n, resolvedBidId: t, rank: s, href: c, target: o = "_self", onclick: i, imageSrc: d, width: a, height: g, style: l = {}, ...u }) { const h = document.createElement("div"); h.dataset.tsBanner = n, h.dataset.tsResolvedId = t, h.dataset.tsRank = s ? s.toString() : "1", h.classList.add("ts-banner"), h.style.width = "100%", h.style.height = "auto", h.style.display = "flex", h.style.justifyContent = "center", h.style.alignItems = "center", r && (h.id = r), e && h.classList.add(e); const f = document.createElement("img"); f.src = d, f.alt = "ts-banner", Object.assign(f.style, l), f.style.width = "100%", f.style.height = "auto", f.style.maxWidth = `${a}px`, f.style.maxHeight = `${g}px`; const p = document.createElement("a"); p.href = c, p.target = o, p.style.width = "100%", p.style.height = "auto", p.style.maxWidth = `${a}px`, p.style.maxHeight = `${g}px`, p.style.display = "inline-block", i && p.addEventListener("click", async (m) => { m.preventDefault(), await i(m), window.location.href = c; }); const y = (m) => m.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); for (const [m, w] of Object.entries(u)) if (typeof w == "string" && p.setAttribute(y(m), w), typeof w == "function" && m.startsWith("on")) { const E = m.slice(2).toLowerCase(); p.addEventListener(E, w); } return C(f, p), C(p, h), h; } const j = { "medium-banner": { width: 300, height: 250 }, "wide-skyscraper": { width: 160, height: 600 }, halfpage: { width: 300, height: 600 }, leaderboard: { width: 728, height: 90 }, "mobile-1": { width: 320, height: 50 }, "mobile-2": { width: 300, height: 250 } }; function z({ slotId: r, width: e, height: n }) { const t = j[r]; if (t) return t; if (e && n) return { width: e, height: n }; } async function W({ target: { target: r, width: e, height: n, position: t = "prepend" }, auction: s }) { const c = z({ slotId: s.slotId, width: e, height: n }); if (!c) return; const o = await _({ // NOTE: theres not real reason to only allow one auction at a time, but we'll keep it as is for now for not creating more complexity auctions: [ { type: "banners", slots: 1, opaqueUserId: x(), ...s } ] }); if (o) { for (const i of o.results) if (i.resultType === "banners") for (const d of i.winners) { const [a] = d.asset; if (!a) continue; const g = D({ slotId: s.slotId, resolvedBidId: d.resolvedBidId, rank: d.rank, href: d.id, imageSrc: a.url, width: c.width, height: c.height, onclick: async () => { await b.event("clicks", { resolvedBidId: d.resolvedBidId }); } }); C(g, r, { position: t }); } return null; } } const X = 0.5; function F({ target: r }) { const e = /* @__PURE__ */ new WeakSet(), n = new IntersectionObserver( (o) => { for (const i of o) i.isIntersecting && (r.event(i.target), e.add(i.target), n.unobserve(i.target)); }, { threshold: r.threshold ?? X } ), t = (o) => { for (const i of Array.from(o)) e.has(i) || n.observe(i); }, s = document.querySelectorAll(r.target); t(s); const c = new MutationObserver((o) => { for (const i of o) if (i.type === "childList") { const d = document.querySelectorAll(r.target); if (!d) return; t(d); } }); return c.observe(document.body, { childList: !0, subtree: !0, attributeFilter: [r.target] }), () => { n.disconnect(), c.disconnect(); }; } class J { constructor(e) { this.targets = [], this.observer = null, this.pollInterval = null, this.lastData = /* @__PURE__ */ new Map(), this.interval = 1e3, this.checkCache = () => { var s; const t = (s = window.__APOLLO_CLIENT__) == null ? void 0 : s.cache.data; if (t) for (const [c, o] of Object.entries(t)) for (const i of this.targets) { const d = c.split(":")[0]; if (d && i.cacheKeyMatcher(d)) { const a = this.lastData.get(d), g = Object.keys(o).length; g > (a ?? 0) && (this.lastData.set(d, g), i.onIntercept({ data: o, key: d })); } } }, e && (this.interval = e), this.observeCache(); } addTarget(e) { this.targets.push(e); } clear() { this.targets = [], this.disconnect(); } observeCache() { this.checkCache(), this.pollInterval = window.setInterval(this.checkCache, this.interval); } disconnect() { this.observer && (this.observer.disconnect(), this.observer = null), this.pollInterval && (window.clearInterval(this.pollInterval), this.pollInterval = null), this.lastData.clear(); } } let K = class { constructor() { this.targets = [], this.originalFetch = fetch.bind(window), this.intercept(); } clear() { this.targets = []; } addTarget(e) { this.targets.push(e); } intercept() { window.fetch = async (e, n) => { var c; const t = typeof e == "string" ? e : e instanceof URL ? e.toString() : e.url, s = (n == null ? void 0 : n.method) || "GET"; try { let o; if (n != null && n.body) try { o = JSON.parse(n.body.toString()); } catch { o = n.body; } const i = await this.originalFetch(e, n), d = i.clone(); for (const a of this.targets) a.urlMatcher(t) && a.methodMatcher(s) && await a.onIntercept({ response: d, request: { url: t, method: s, headers: new Headers(n == null ? void 0 : n.headers), body: o } }); return i; } catch (o) { for (const i of this.targets) i.urlMatcher(t) && i.methodMatcher(s) && ((c = i.onError) == null || c.call(i, o instanceof Error ? o : new Error(String(o)))); throw o; } }; } }; const V = window.XMLHttpRequest; class G { constructor() { this.targets = [], this.intercept(); } clear() { this.targets = []; } addTarget(e) { this.targets.push(e); } intercept() { const e = this; function n() { const t = new V(), s = t.open, c = t.send, o = t.setRequestHeader; let i = "", d = "", a; const g = {}; return t.setRequestHeader = (l, u) => (g[l.toLowerCase()] = u, o.call(t, l, u)), t.open = (l, u, ...h) => (i = l, d = u, s.apply(t, [l, u, ...h])), t.addEventListener("load", () => { var l; for (const u of e.targets) if (u.urlMatcher(d) && u.methodMatcher(i)) { const h = {}, f = t.getAllResponseHeaders().split(`\r `); for (const p of f) { const [y, m] = p.split(": "); y && m && (h[y.toLowerCase()] = m); } (l = u.onIntercept) == null || l.call(u, { response: { status: t.status, statusText: t.statusText, responseText: t.responseText, responseURL: t.responseURL, headers: h }, payload: a, requestHeaders: g, url: d }); } }), t.addEventListener("error", () => { var l; for (const u of e.targets) u.urlMatcher(d) && u.methodMatcher(i) && ((l = u.onError) == null || l.call(u, new Error(`XHR Error: ${t.statusText}`))); }), t.send = (l) => (a = l, c.call(t, l)), t; } window.XMLHttpRequest = n; } } class Y { constructor() { this.events = [], this.interceptorXHR = new G(), this.interceptorFetch = new K(), this.interceptorApolloCache = new J(); } addEvent(e) { e.type === "dom" ? (F({ target: { target: e.target, event: e.onEvent, threshold: e.threshold } }), this.events.push({ options: e })) : e.type === "xhr" ? (this.interceptorXHR.addTarget(e), this.events.push({ options: e })) : e.type === "fetch" ? (this.interceptorFetch.addTarget(e), this.events.push({ options: e })) : e.type === "apollo-cache" && (this.interceptorApolloCache.addTarget(e), this.events.push({ options: e })); } clear() { this.events = [], this.interceptorXHR.clear(), this.interceptorFetch.clear(); } } const Z = 5; function Q(r) { var t, s; const e = document.querySelectorAll(".MenuCategoryCarousels__carousel"); for (const c of Array.from(e)) { const o = c.querySelector(".MenuCategoryCarousels__subtitle__main"); if ((t = o == null ? void 0 : o.textContent) != null && t.toLowerCase().includes(r.toLowerCase())) return c; } const n = document.querySelectorAll(".MenuCatalog"); for (const c of Array.from(n)) { const o = c.querySelector(".MenuCatalog__title"); if ((s = o == null ? void 0 : o.textContent) != null && s.toLowerCase().includes(r.toLowerCase())) return c; } return null; } async function ee(r) { try { const e = x(), n = r.map(() => ({ type: "banners", slots: 1, slotId: "sponsored-listing-banner", opaqueUserId: e, deviceType: I.deviceType })); return await _({ auctions: n }); } catch { return; } } function te(r, e) { const n = []; for (const t of r) e[t] && n.push(e[t].product_id); return n; } function M(r, e) { const n = e.find((t) => t.value.includes(r)); if (n) return n; for (const t of e) { const s = M(r, t.children); if (s) return s; } return null; } function ne({ resolvedBidId: r, imageUrl: e, productName: n }) { const t = document.createElement("div"); t.setAttribute("data-ts-resolved-id", r), t.style.display = "flex", t.style.justifyContent = "center"; const s = document.createElement("img"); return s.src = e, s.alt = n, s.style.height = "160px", s.style.width = "fit-content", t.addEventListener("click", () => { b.event("clicks", { resolvedBidId: r }); }), t.appendChild(s), t; } async function re(r, e) { var n; try { const t = JSON.parse(r), s = window.location.pathname.split("/").pop(); if (!s) return; const c = M(s, t.data.categories); if (!c) return; let o = c.children.slice(0, Z); o.length === 0 && c.product_ids.length > 0 && (o = [c]); const i = o.map((a) => ({ products_ids: te(a.product_ids, t.data.products), slotsNumber: e, category: a })), d = await ee(i); if (!d) return; for (const a of d.results) { if (a.resultType !== "banners" || a.winners.length === 0) continue; const g = d.results.indexOf(a), l = i[g]; if (!l) continue; const u = Q(l.category.name); if (!u) continue; const h = u.querySelectorAll(".MenuItem"); if (h) for (const f of a.winners) { const p = Object.values(t.data.products).find( (v) => v.product_id === f.id ); if (!p) continue; const y = l.category.product_ids.findIndex((v) => v === p.id); if (y === -1) continue; const m = u.querySelector("[data-ts-resolved-id]"); let w; if (m ? w = m.parentElement : w = h[y], !w) continue; if (!m) { const v = (n = f.asset) == null ? void 0 : n[0]; if (!v) continue; const U = ne({ resolvedBidId: f.resolvedBidId, imageUrl: v.url, productName: p.name }), T = w.querySelector("img.MenuItem__image"); if (!T) continue; w.replaceChild(U, T); const k = w.querySelector(".MenuItem__sizes"); k && k.remove(); } const E = h[f.rank - 1]; if (E) try { const v = E.parentElement; v && v.insertBefore(w, E); } catch { return; } } } return; } catch { return; } } async function se(r) { try { const e = x(), n = r.map(() => ({ type: "banners", slots: 1, slotId: "sponsored-listing-banner", opaqueUserId: e, deviceType: I.deviceType })); return await _({ auctions: n }); } catch { return; } } function oe({ resolvedBidId: r, imageUrl: e, productName: n }) { const t = document.createElement("div"); t.setAttribute("data-ts-resolved-id", r), t.style.display = "flex", t.style.justifyContent = "center"; const s = document.createElement("img"); return s.src = e, s.alt = n, s.style.height = "160px", s.style.width = "fit-content", t.addEventListener("click", () => { b.event("clicks", { resolvedBidId: r }); }), t.appendChild(s), t; } async function ie(r, e) { var n; try { const t = JSON.parse(r); if (!t.data.search) return; const s = t.data.search.ids.filter((a) => a !== null), c = []; for (const a of s) t.data.products[a] && c.push(t.data.products[a].product_id); const o = await se([{ products_ids: c, slotsNumber: e }]); if (!o || !o.results) return; const i = document.querySelector(".MenuCatalog__container"); if (!i) return; const d = i.querySelectorAll(".MenuItem"); if (!d) return; for (const a of o.results) if (a.resultType === "banners" && a.winners.length !== 0) for (const g of a.winners) { const l = Object.values(t.data.products).find( (y) => (y == null ? void 0 : y.product_id) === g.id ); if (!l) continue; const u = s.findIndex((y) => y === l.id); if (u === -1) continue; const h = i.querySelector("[data-ts-resolved-id]"); let f; if (h ? f = h.parentElement : f = d[u], !f) continue; if (!h) { const y = (n = g.asset) == null ? void 0 : n[0]; if (!y) continue; const m = oe({ resolvedBidId: g.resolvedBidId, imageUrl: y.url, productName: l.name }), w = f.querySelector("img.MenuItem__image"); if (!w) continue; f.replaceChild(m, w); const E = f.querySelector(".MenuItem__sizes"); E && E.remove(); } const p = d[g.rank - 1]; if (p) try { const y = p.parentElement; y && y.insertBefore(f, p); } catch { return; } } return; } catch { return; } } function R({ responseText: r }) { try { const e = JSON.parse(r); if (Object.keys(e.data.products).length === 0) return; S("v1/products", { method: "POST", body: JSON.stringify({ data: { products: e.data.products } }) }); } catch { return; } } async function ce({ orderId: r, clientId: e, authorization: n }) { try { return (await fetch( `/api/customer/orders/recent/${r}?include_merchant=true&client_id=${e}`, { headers: { Authorization: n } } )).json(); } catch { return null; } } async function ae({ responseText: r, requestHeaders: e, url: n }) { var g; const t = Object.keys(e).find( (l) => l.toLowerCase() === "authorization" ); if (!t) return; const s = e[t], c = n.match(/[?&]client_id=([^&]+)/), o = c ? c[1] : null; if (!o) return; const { orders: i } = JSON.parse(r); if (!i) return; const d = await Promise.all( i.map(async (l) => await ce({ orderId: l.orderId.toString(), clientId: o, authorization: s })) ), a = []; for (const l of d) if ((g = l == null ? void 0 : l.order) != null && g.cart) for (const u of l.order.cart) a.push({ productId: u.id, unitPrice: Number.parseFloat(u.unit_price), quantity: u.quantity }); b.event("purchases", { items: a }); } function de() { return [ /** * Search page Sponsored Banner */ { type: "xhr", urlMatcher: (e) => { const n = e.includes("/api/data/search"), t = e.includes("keyword="); return !!(n && t); }, methodMatcher: (e) => e === "GET", onIntercept: async ({ response: e }) => { ie(e.responseText, 1), I.enabledCatalogIngestion && R({ responseText: e.responseText }); } }, /** * Category page Sponsored Banner */ { type: "xhr", urlMatcher: (e) => { const n = e.includes("/api/data/search"), t = e.includes("keyword="); return !!(n && !t); }, methodMatcher: (e) => e === "GET", onIntercept: async ({ response: e }) => { re(e.responseText, 1), I.enabledCatalogIngestion && R({ responseText: e.responseText }); } }, /** * Checkout/purchase event */ { type: "xhr", urlMatcher: (e) => e.includes("/api/customer/checkout"), methodMatcher: (e) => e === "GET", onIntercept: async ({ response: e, requestHeaders: n, url: t }) => { ae({ responseText: e.responseText, requestHeaders: n, url: t }); } } ]; } function le() { return [ // { // type: "apollo-cache", // cacheKeyMatcher: (key) => { // if (key.startsWith("Items")) { // return true; // } // return false; // }, // onIntercept: async ({ data }) => { // const cachedData = getCache("CollectionProductsWithFeaturedProducts"); // if (cachedData) { // categorySponsorBanner( // "sponsored-listing-banner", // cachedData as Record<string, unknown>, // data as Record<string, unknown>, // 1, // ); // } // }, // }, // { // type: "apollo-cache", // cacheKeyMatcher: (key) => { // if (key.startsWith("CollectionProductsWithFeaturedProducts")) { // return true; // } // return false; // }, // onIntercept: async ({ data }) => { // setCache("CollectionProductsWithFeaturedProducts", data); // }, // }, ]; } function ue(r) { switch (r) { case "TS1003581": return de(); case "TS1004020": return le(); default: return; } } class he { /** * Initializes the Toppie SDK * @param opts.appId - The ID of the app to initialize * @param opts.debug - Whether to enable debug mode * @param opts.cookieName - The name of the cookie to use for the user ID */ constructor() { this.config = I, this.eventCollector = new Y(); } init(e) { this.config.setAppId(e.appId), e.debug && this.config.setDebug(e.debug), e.cookieName && this.config.setCookieName(e.cookieName), e.userId && this.config.setUserId(e.userId), e.baseURL && this.config.setBaseURL(e.baseURL), this.preload(); } /** * Preloads system events and app specific custom events */ preload() { const e = this.config.getAppId(); if (!e) return; this.eventCollector.addEvent({ type: "dom", target: "[data-ts-resolved-id]", onEvent: (t) => { const s = t.getAttribute("data-ts-resolved-id"); s && b.event("impressions", { resolvedBidId: s }); } }); const n = ue(e); if (n) for (const t of n) this.eventCollector.addEvent(t); } /** * Identifies the user with a unique ID * @param opts.userId - The user's ID */ identify(e) { this.config.setUserId(e.userId); } /** * Adds an ad to the page * @param opts.target.type - The type of ad to add (banners or listings) * @param opts.target.selector - The selector where the ad will be added * @param opts.target.width - (optional) The width of the ad * @param opts.target.height - (optional) The height of the ad * @param opts.target.position - (optional) The position of the ad (append or prepend) * @param opts.auction - The auction options as defined in the auction service https://api.docs.topsort.com/api-reference/auctions/create-auctions */ auctions(e) { e.type === "banners" && this.eventCollector.addEvent({ type: "dom", target: e.target.selector, onEvent: (n) => { W({ target: { target: n, width: e.target.width, height: e.target.height, position: e.target.position }, auction: e.auction }); }, // NOTE: for inserting banners we need to trigger asap threshold: 0 }); } } const fe = new he(); window.toppie = fe; export { fe as toppie };