@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
JavaScript
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
};