@zerocost/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/client.d.ts +11 -0
- package/dist/core/config.d.ts +2 -0
- package/dist/core/consent.d.ts +7 -0
- package/dist/index.cjs +741 -14
- package/dist/index.d.ts +38 -42
- package/dist/index.js +738 -14
- package/dist/modules/ads.d.ts +11 -0
- package/dist/modules/llm-data.d.ts +39 -0
- package/dist/modules/recording.d.ts +30 -0
- package/dist/modules/trackers.d.ts +17 -0
- package/dist/modules/widget.d.ts +52 -0
- package/dist/types/index.d.ts +46 -0
- package/package.json +24 -24
- package/dist/index.d.cts +0 -45
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
var EDGE_FUNCTION_BASE = "https://mwbgzpbuoojqsuxduieo.supabase.co/functions/v1";
|
|
3
|
+
function getBaseUrl(custom) {
|
|
4
|
+
return custom || EDGE_FUNCTION_BASE;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
// src/core/client.ts
|
|
2
8
|
var ZerocostClient = class {
|
|
3
9
|
config;
|
|
10
|
+
baseUrl;
|
|
4
11
|
isInitialized = false;
|
|
5
12
|
constructor(config) {
|
|
6
13
|
if (!config.appId) {
|
|
7
14
|
throw new Error("ZerocostSDK: appId is required");
|
|
8
15
|
}
|
|
16
|
+
if (!config.apiKey) {
|
|
17
|
+
throw new Error("ZerocostSDK: apiKey is required");
|
|
18
|
+
}
|
|
9
19
|
this.config = {
|
|
10
20
|
environment: "production",
|
|
11
21
|
debug: false,
|
|
12
22
|
...config
|
|
13
23
|
};
|
|
24
|
+
this.baseUrl = getBaseUrl(config.baseUrl);
|
|
14
25
|
}
|
|
15
26
|
init() {
|
|
16
27
|
if (this.isInitialized) {
|
|
@@ -18,40 +29,753 @@ var ZerocostClient = class {
|
|
|
18
29
|
return;
|
|
19
30
|
}
|
|
20
31
|
this.isInitialized = true;
|
|
21
|
-
this.log(`ZerocostSDK
|
|
32
|
+
this.log(`ZerocostSDK initialized for ${this.config.appId} in ${this.config.environment} mode.`);
|
|
22
33
|
}
|
|
23
34
|
getConfig() {
|
|
24
35
|
return this.config;
|
|
25
36
|
}
|
|
37
|
+
async request(path, body) {
|
|
38
|
+
const url = `${this.baseUrl}${path}`;
|
|
39
|
+
this.log(`\u2192 ${url}`, body);
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"x-api-key": this.config.apiKey
|
|
45
|
+
},
|
|
46
|
+
body: body ? JSON.stringify(body) : void 0
|
|
47
|
+
});
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
this.log(`\u2717 ${res.status}`, data);
|
|
51
|
+
throw new Error(data.error || `Request failed with status ${res.status}`);
|
|
52
|
+
}
|
|
53
|
+
this.log(`\u2713 ${res.status}`, data);
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
26
56
|
log(message, data) {
|
|
27
57
|
if (this.config.debug) {
|
|
28
|
-
console.log(`[Zerocost] ${message}`, data
|
|
58
|
+
console.log(`[Zerocost] ${message}`, data ?? "");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/modules/ads.ts
|
|
64
|
+
var AdsModule = class {
|
|
65
|
+
constructor(client) {
|
|
66
|
+
this.client = client;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Request an ad for a given placement.
|
|
70
|
+
* Returns ad content including headline, body, CTA, and tracking pixel.
|
|
71
|
+
*/
|
|
72
|
+
async requestAd(placementId) {
|
|
73
|
+
const data = await this.client.request("/serve-ad", {
|
|
74
|
+
placement_id: placementId
|
|
75
|
+
});
|
|
76
|
+
return data.ad;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/modules/trackers.ts
|
|
81
|
+
var TrackModule = class {
|
|
82
|
+
constructor(client) {
|
|
83
|
+
this.client = client;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Track a custom event with optional properties.
|
|
87
|
+
*/
|
|
88
|
+
async event(eventName, properties) {
|
|
89
|
+
await this.client.request("/track-event", {
|
|
90
|
+
event_name: eventName,
|
|
91
|
+
properties: properties || {}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Track an ad impression.
|
|
96
|
+
*/
|
|
97
|
+
async impression(adId, placementId) {
|
|
98
|
+
await this.event("ad_impression", { ad_id: adId, placement_id: placementId });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Track an ad click.
|
|
102
|
+
*/
|
|
103
|
+
async click(adId, placementId) {
|
|
104
|
+
await this.event("ad_click", { ad_id: adId, placement_id: placementId });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/modules/widget.ts
|
|
109
|
+
var POSITION_STYLES = {
|
|
110
|
+
"bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
|
|
111
|
+
"bottom-left": "position:fixed;bottom:24px;left:24px;z-index:9999;",
|
|
112
|
+
"top-right": "position:fixed;top:24px;right:24px;z-index:9999;",
|
|
113
|
+
"top-left": "position:fixed;top:24px;left:24px;z-index:9999;",
|
|
114
|
+
"bottom-center": "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;",
|
|
115
|
+
"top-center": "position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:9999;",
|
|
116
|
+
"center": "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;",
|
|
117
|
+
"sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
|
|
118
|
+
"sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
|
|
119
|
+
};
|
|
120
|
+
var WidgetModule = class {
|
|
121
|
+
constructor(client) {
|
|
122
|
+
this.client = client;
|
|
123
|
+
}
|
|
124
|
+
mounted = /* @__PURE__ */ new Map();
|
|
125
|
+
async autoInjectWithConfig(display) {
|
|
126
|
+
try {
|
|
127
|
+
const configs = this.normalizeConfigs(display);
|
|
128
|
+
const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
|
|
129
|
+
this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
|
|
130
|
+
await this.mountFormats(configs);
|
|
131
|
+
this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.client.log(`Widget autoInject error: ${err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async autoInject() {
|
|
137
|
+
try {
|
|
138
|
+
this.client.log("Fetching ad placements from server...");
|
|
139
|
+
const { display } = await this.client.request("/get-placements");
|
|
140
|
+
const configs = this.normalizeConfigs(display);
|
|
141
|
+
const enabledCount = Object.values(configs).filter((c) => c.enabled).length;
|
|
142
|
+
this.client.log(`Auto-inject: ${enabledCount} ad format(s) enabled. No custom component needed \u2014 ads render automatically.`);
|
|
143
|
+
await this.mountFormats(configs);
|
|
144
|
+
this.client.log("\u2713 Ad slots injected successfully. Ads will appear once inventory is available.");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
this.client.log(`Widget autoInject error: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
normalizeConfigs(display) {
|
|
150
|
+
if (display && "video-widget" in display) {
|
|
151
|
+
return display;
|
|
152
|
+
}
|
|
153
|
+
const pos = display?.position || "bottom-right";
|
|
154
|
+
const theme = display?.theme || "dark";
|
|
155
|
+
return {
|
|
156
|
+
"video-widget": { position: pos, theme, autoplay: false, enabled: true },
|
|
157
|
+
"tooltip-ad": { position: pos, theme, autoplay: false, enabled: true },
|
|
158
|
+
"sponsored-card": { position: pos, theme, autoplay: false, enabled: true },
|
|
159
|
+
"inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: true }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async mountFormats(configs) {
|
|
163
|
+
for (const [format, cfg] of Object.entries(configs)) {
|
|
164
|
+
if (!cfg.enabled) continue;
|
|
165
|
+
const elId = `zerocost-${format}`;
|
|
166
|
+
if (document.getElementById(elId)) continue;
|
|
167
|
+
const el = document.createElement("div");
|
|
168
|
+
el.id = elId;
|
|
169
|
+
el.setAttribute("data-zerocost", "");
|
|
170
|
+
el.setAttribute("data-format", format);
|
|
171
|
+
if (format === "inline-text") {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const posStyle = POSITION_STYLES[cfg.position] || POSITION_STYLES["bottom-right"];
|
|
175
|
+
const maxW = format === "video-widget" ? "max-width:200px;" : format === "sponsored-card" || format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
|
|
176
|
+
el.setAttribute("style", posStyle + maxW);
|
|
177
|
+
document.body.appendChild(el);
|
|
178
|
+
await this.mount(elId, { format, refreshInterval: 30, theme: cfg.theme, autoplay: cfg.autoplay });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async mount(targetElementId, options = {}) {
|
|
182
|
+
const el = document.getElementById(targetElementId);
|
|
183
|
+
if (!el) return;
|
|
184
|
+
if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
|
|
185
|
+
const refreshMs = (options.refreshInterval ?? 30) * 1e3;
|
|
186
|
+
const theme = options.theme || "dark";
|
|
187
|
+
const format = options.format || "video-widget";
|
|
188
|
+
const autoplay = options.autoplay ?? false;
|
|
189
|
+
const render = async () => {
|
|
190
|
+
try {
|
|
191
|
+
const formatMap = {
|
|
192
|
+
"video-widget": "video",
|
|
193
|
+
"sponsored-card": "display",
|
|
194
|
+
"sidebar-display": "display",
|
|
195
|
+
"tooltip-ad": "native",
|
|
196
|
+
"inline-text": "native"
|
|
197
|
+
};
|
|
198
|
+
const body = {
|
|
199
|
+
format: formatMap[format] || format,
|
|
200
|
+
theme,
|
|
201
|
+
autoplay
|
|
202
|
+
};
|
|
203
|
+
const data = await this.client.request("/serve-widget", body);
|
|
204
|
+
const ad = data.ad;
|
|
205
|
+
if (!ad || !data.html) {
|
|
206
|
+
this.client.log(`No ad inventory available for format "${format}". Slot "${targetElementId}" is empty \u2014 ads will appear automatically when inventory is added by partners.`);
|
|
207
|
+
el.innerHTML = "";
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
el.innerHTML = data.html;
|
|
211
|
+
el.setAttribute("data-zerocost-ad-id", ad.id);
|
|
212
|
+
const ctas = el.querySelectorAll("[data-zc-cta]");
|
|
213
|
+
ctas.forEach((cta) => {
|
|
214
|
+
cta.addEventListener("click", () => {
|
|
215
|
+
this.client.request("/track-event", {
|
|
216
|
+
event_name: "ad_click",
|
|
217
|
+
properties: { ad_id: ad.id, format }
|
|
218
|
+
}).catch(() => {
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
const closeBtn = el.querySelector("[data-zc-close]");
|
|
223
|
+
if (closeBtn) {
|
|
224
|
+
closeBtn.addEventListener("click", (e) => {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
el.innerHTML = "";
|
|
228
|
+
el.style.display = "none";
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.client.log(`Widget render error: ${err}`);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
await render();
|
|
236
|
+
const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
|
|
237
|
+
this.mounted.set(targetElementId, { elementId: targetElementId, interval });
|
|
238
|
+
}
|
|
239
|
+
unmount(targetElementId) {
|
|
240
|
+
const slot = this.mounted.get(targetElementId);
|
|
241
|
+
if (slot) {
|
|
242
|
+
if (slot.interval) clearInterval(slot.interval);
|
|
243
|
+
const el = document.getElementById(targetElementId);
|
|
244
|
+
if (el) el.remove();
|
|
245
|
+
this.mounted.delete(targetElementId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
unmountAll() {
|
|
249
|
+
for (const id of this.mounted.keys()) this.unmount(id);
|
|
250
|
+
}
|
|
251
|
+
// ─── Format-specific renderers (matching SDK Playground previews exactly) ───
|
|
252
|
+
buildFormatHtml(format, ad, theme, autoplay) {
|
|
253
|
+
switch (format) {
|
|
254
|
+
case "video-widget":
|
|
255
|
+
return this.buildVideoWidget(ad, theme, autoplay);
|
|
256
|
+
case "tooltip-ad":
|
|
257
|
+
return this.buildTooltipAd(ad, theme);
|
|
258
|
+
case "sponsored-card":
|
|
259
|
+
return this.buildSponsoredCard(ad, theme);
|
|
260
|
+
case "inline-text":
|
|
261
|
+
return this.buildInlineText(ad, theme);
|
|
262
|
+
default:
|
|
263
|
+
return this.buildSponsoredCard(ad, theme);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Floating Video Widget — matches playground VideoWidgetPreview
|
|
268
|
+
* 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
|
|
269
|
+
*/
|
|
270
|
+
buildVideoWidget(ad, theme, autoplay) {
|
|
271
|
+
const isDark = theme === "dark";
|
|
272
|
+
const border = isDark ? "#333" : "#e0e0e0";
|
|
273
|
+
const videoSrc = ad.video_url || ad.image_url || "";
|
|
274
|
+
const hasVideo = !!ad.video_url;
|
|
275
|
+
const sponsor = ad.title || "Sponsor";
|
|
276
|
+
const cta = ad.cta_text || "Learn More";
|
|
277
|
+
return `
|
|
278
|
+
<div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
|
|
279
|
+
<button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
|
|
280
|
+
${hasVideo ? `<video src="${this.esc(videoSrc)}" ${autoplay ? "autoplay" : ""} muted loop playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
|
|
281
|
+
<div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
|
|
282
|
+
<div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
|
|
283
|
+
<div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
|
|
284
|
+
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
|
|
285
|
+
</div>
|
|
286
|
+
</div>`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Tooltip Ad — matches playground ContextualTextPreview
|
|
290
|
+
* Small floating card with sponsored label, inline text, and link
|
|
291
|
+
*/
|
|
292
|
+
buildTooltipAd(ad, theme) {
|
|
293
|
+
const isDark = theme === "dark";
|
|
294
|
+
const bg = isDark ? "#111" : "#ffffff";
|
|
295
|
+
const fg = isDark ? "#fff" : "#111";
|
|
296
|
+
const fgFaint = isDark ? "#888" : "#999";
|
|
297
|
+
const border = isDark ? "#333" : "#e0e0e0";
|
|
298
|
+
return `
|
|
299
|
+
<div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
|
|
300
|
+
<div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
|
|
301
|
+
<span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
|
|
302
|
+
Sponsored
|
|
303
|
+
</div>
|
|
304
|
+
<p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
|
|
305
|
+
${this.esc(ad.description || ad.title)}
|
|
306
|
+
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
307
|
+
</p>
|
|
308
|
+
</div>`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Sponsored Card — matches playground SponsoredCardPreview
|
|
312
|
+
* Card with gradient header, icon, headline, description, CTA button
|
|
313
|
+
*/
|
|
314
|
+
buildSponsoredCard(ad, theme) {
|
|
315
|
+
const isDark = theme === "dark";
|
|
316
|
+
const bg = isDark ? "#111" : "#ffffff";
|
|
317
|
+
const fg = isDark ? "#fff" : "#111";
|
|
318
|
+
const fgFaint = isDark ? "#888" : "#999";
|
|
319
|
+
const border = isDark ? "#333" : "#e0e0e0";
|
|
320
|
+
const iconBg = isDark ? "#0a0a0a" : "#f5f5f5";
|
|
321
|
+
const initial = (ad.title || "A").charAt(0).toUpperCase();
|
|
322
|
+
return `
|
|
323
|
+
<div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;">
|
|
324
|
+
${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
|
|
325
|
+
<div style="padding:12px;">
|
|
326
|
+
<div style="width:24px;height:24px;border-radius:4px;background:${iconBg};border:1px solid ${border};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:9px;color:${fg};margin-bottom:8px;">${initial}</div>
|
|
327
|
+
<div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
|
|
328
|
+
<div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
|
|
329
|
+
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
330
|
+
</div>
|
|
331
|
+
</div>`;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Inline Text Ad — matches playground InlineTextPreview
|
|
335
|
+
* Card with sponsored header, icon, headline, body, CTA button
|
|
336
|
+
* Designed to be injected inside content/chat streams
|
|
337
|
+
*/
|
|
338
|
+
buildInlineText(ad, theme) {
|
|
339
|
+
const isDark = theme === "dark";
|
|
340
|
+
const bg = isDark ? "#141414" : "#fafafa";
|
|
341
|
+
const fg = isDark ? "#fff" : "#111";
|
|
342
|
+
const fgMuted = isDark ? "#aaa" : "#666";
|
|
343
|
+
const fgFaint = isDark ? "#888" : "#999";
|
|
344
|
+
const border = isDark ? "#2a2a2a" : "#d0d0d0";
|
|
345
|
+
const headerBorder = isDark ? "#222" : "#e8e8e8";
|
|
346
|
+
const btnBg = isDark ? "#fff" : "#111";
|
|
347
|
+
const btnFg = isDark ? "#111" : "#fff";
|
|
348
|
+
const initial = "\u25B2";
|
|
349
|
+
const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
|
|
350
|
+
return `
|
|
351
|
+
<div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
|
|
352
|
+
<div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
|
|
353
|
+
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
|
|
354
|
+
<span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
|
|
355
|
+
</div>
|
|
356
|
+
<div style="padding:12px;">
|
|
357
|
+
<div style="display:flex;gap:10px;">
|
|
358
|
+
<div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
|
|
359
|
+
<div style="flex:1;min-width:0;">
|
|
360
|
+
<div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
|
|
361
|
+
<div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
|
|
362
|
+
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>`;
|
|
367
|
+
}
|
|
368
|
+
esc(str) {
|
|
369
|
+
if (!str) return "";
|
|
370
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// src/modules/llm-data.ts
|
|
375
|
+
var LLMDataModule = class {
|
|
376
|
+
constructor(client) {
|
|
377
|
+
this.client = client;
|
|
378
|
+
}
|
|
379
|
+
config = null;
|
|
380
|
+
buffer = [];
|
|
381
|
+
flushInterval = null;
|
|
382
|
+
clickHandler = null;
|
|
383
|
+
errorHandler = null;
|
|
384
|
+
fetchOriginal = null;
|
|
385
|
+
/**
|
|
386
|
+
* Start capturing LLM training data based on server config.
|
|
387
|
+
*/
|
|
388
|
+
start(config) {
|
|
389
|
+
this.config = config;
|
|
390
|
+
this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
|
|
391
|
+
if (Math.random() * 100 > config.sampleRate) {
|
|
392
|
+
this.client.log("LLMData: session not sampled, skipping");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (config.uiInteractions) this.captureUIInteractions();
|
|
396
|
+
if (config.textPrompts) this.interceptPrompts();
|
|
397
|
+
if (config.apiErrors) this.captureAPIErrors();
|
|
398
|
+
this.flushInterval = setInterval(() => this.flush(), 1e4);
|
|
399
|
+
}
|
|
400
|
+
stop() {
|
|
401
|
+
if (this.flushInterval) clearInterval(this.flushInterval);
|
|
402
|
+
if (this.clickHandler) {
|
|
403
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
404
|
+
}
|
|
405
|
+
if (this.errorHandler) {
|
|
406
|
+
window.removeEventListener("error", this.errorHandler);
|
|
407
|
+
}
|
|
408
|
+
if (this.fetchOriginal) {
|
|
409
|
+
window.fetch = this.fetchOriginal;
|
|
410
|
+
}
|
|
411
|
+
this.flush();
|
|
412
|
+
this.config = null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Manually track an LLM prompt/response pair (for startups that want to
|
|
416
|
+
* explicitly send their AI interactions).
|
|
417
|
+
*/
|
|
418
|
+
trackPrompt(prompt, response, meta) {
|
|
419
|
+
if (!this.config?.textPrompts) return;
|
|
420
|
+
this.pushEvent("llm_prompt", {
|
|
421
|
+
prompt: this.scrub(prompt),
|
|
422
|
+
response: response ? this.scrub(response) : void 0,
|
|
423
|
+
...meta
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Manually track an API error.
|
|
428
|
+
*/
|
|
429
|
+
trackError(endpoint, status, message) {
|
|
430
|
+
if (!this.config?.apiErrors) return;
|
|
431
|
+
this.pushEvent("api_error", {
|
|
432
|
+
endpoint,
|
|
433
|
+
status,
|
|
434
|
+
message: message ? this.scrub(message) : void 0
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// ── Private ──
|
|
438
|
+
captureUIInteractions() {
|
|
439
|
+
this.clickHandler = (e) => {
|
|
440
|
+
const target = e.target;
|
|
441
|
+
if (!target) return;
|
|
442
|
+
const tag = target.tagName?.toLowerCase();
|
|
443
|
+
const text = target.textContent?.slice(0, 50)?.trim() || "";
|
|
444
|
+
const role = target.getAttribute("role");
|
|
445
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
446
|
+
const path = this.getPath(target);
|
|
447
|
+
this.pushEvent("ui_interaction", {
|
|
448
|
+
action: "click",
|
|
449
|
+
tag,
|
|
450
|
+
text: this.scrub(text),
|
|
451
|
+
role,
|
|
452
|
+
ariaLabel,
|
|
453
|
+
path,
|
|
454
|
+
url: location.pathname
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
458
|
+
}
|
|
459
|
+
interceptPrompts() {
|
|
460
|
+
this.fetchOriginal = window.fetch;
|
|
461
|
+
const self = this;
|
|
462
|
+
const origFetch = window.fetch;
|
|
463
|
+
window.fetch = async function(input, init) {
|
|
464
|
+
const fetchInput = input instanceof URL ? input.toString() : input;
|
|
465
|
+
const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
|
|
466
|
+
const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
|
|
467
|
+
if (!isLLM) return origFetch.call(window, fetchInput, init);
|
|
468
|
+
try {
|
|
469
|
+
const res = await origFetch.call(window, fetchInput, init);
|
|
470
|
+
const clone = res.clone();
|
|
471
|
+
let reqBody;
|
|
472
|
+
if (init?.body && typeof init.body === "string") {
|
|
473
|
+
try {
|
|
474
|
+
const parsed = JSON.parse(init.body);
|
|
475
|
+
reqBody = JSON.stringify(parsed.messages || parsed.prompt || "").slice(0, 500);
|
|
476
|
+
} catch {
|
|
477
|
+
reqBody = void 0;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
clone.text().then((text) => {
|
|
481
|
+
self.pushEvent("llm_prompt", {
|
|
482
|
+
endpoint: new URL(url).pathname,
|
|
483
|
+
request: reqBody ? self.scrub(reqBody) : void 0,
|
|
484
|
+
response: self.scrub(text.slice(0, 500)),
|
|
485
|
+
status: res.status
|
|
486
|
+
});
|
|
487
|
+
}).catch(() => {
|
|
488
|
+
});
|
|
489
|
+
return res;
|
|
490
|
+
} catch (err) {
|
|
491
|
+
self.pushEvent("api_error", {
|
|
492
|
+
endpoint: new URL(url).pathname,
|
|
493
|
+
message: self.scrub(err.message)
|
|
494
|
+
});
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
captureAPIErrors() {
|
|
500
|
+
this.errorHandler = (e) => {
|
|
501
|
+
this.pushEvent("api_error", {
|
|
502
|
+
message: this.scrub(e.message || ""),
|
|
503
|
+
filename: e.filename,
|
|
504
|
+
line: e.lineno,
|
|
505
|
+
url: location.pathname
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
window.addEventListener("error", this.errorHandler);
|
|
509
|
+
}
|
|
510
|
+
pushEvent(type, data) {
|
|
511
|
+
this.buffer.push({ type, data, timestamp: Date.now() });
|
|
512
|
+
if (this.buffer.length >= 50) this.flush();
|
|
513
|
+
}
|
|
514
|
+
async flush() {
|
|
515
|
+
if (this.buffer.length === 0) return;
|
|
516
|
+
const events = [...this.buffer];
|
|
517
|
+
this.buffer = [];
|
|
518
|
+
try {
|
|
519
|
+
await this.client.request("/ingest-data", { type: "llm", events });
|
|
520
|
+
} catch (err) {
|
|
521
|
+
this.client.log(`LLMData flush error: ${err}`);
|
|
522
|
+
this.buffer.unshift(...events);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/** Basic PII scrubbing */
|
|
526
|
+
scrub(text) {
|
|
527
|
+
return text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[EMAIL]").replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, "[PHONE]").replace(/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, "[CARD]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]").replace(/\b(sk|pk|api|key|secret|token)[-_]?[a-zA-Z0-9]{16,}\b/gi, "[API_KEY]");
|
|
528
|
+
}
|
|
529
|
+
getPath(el) {
|
|
530
|
+
const parts = [];
|
|
531
|
+
let cur = el;
|
|
532
|
+
while (cur && parts.length < 5) {
|
|
533
|
+
let seg = cur.tagName?.toLowerCase() || "";
|
|
534
|
+
if (cur.id) seg += `#${cur.id}`;
|
|
535
|
+
else if (cur.className && typeof cur.className === "string") {
|
|
536
|
+
const cls = cur.className.split(" ")[0];
|
|
537
|
+
if (cls) seg += `.${cls}`;
|
|
538
|
+
}
|
|
539
|
+
parts.unshift(seg);
|
|
540
|
+
cur = cur.parentElement;
|
|
541
|
+
}
|
|
542
|
+
return parts.join(" > ");
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// src/modules/recording.ts
|
|
547
|
+
var RecordingModule = class {
|
|
548
|
+
constructor(client) {
|
|
549
|
+
this.client = client;
|
|
550
|
+
}
|
|
551
|
+
config = null;
|
|
552
|
+
events = [];
|
|
553
|
+
observer = null;
|
|
554
|
+
flushInterval = null;
|
|
555
|
+
stopTimer = null;
|
|
556
|
+
handlers = [];
|
|
557
|
+
sessionId = "";
|
|
558
|
+
startTime = 0;
|
|
559
|
+
/**
|
|
560
|
+
* Start a lightweight UX session recording.
|
|
561
|
+
*/
|
|
562
|
+
start(config) {
|
|
563
|
+
if (typeof document === "undefined") return;
|
|
564
|
+
this.config = config;
|
|
565
|
+
this.sessionId = this.generateId();
|
|
566
|
+
this.startTime = Date.now();
|
|
567
|
+
if (Math.random() * 100 > config.sampleRate) {
|
|
568
|
+
this.client.log("Recording: session not sampled, skipping");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.client.log(`Recording: started session ${this.sessionId} (max ${config.maxDuration}s)`);
|
|
572
|
+
this.pushEvent("resize", {
|
|
573
|
+
width: window.innerWidth,
|
|
574
|
+
height: window.innerHeight,
|
|
575
|
+
url: location.href,
|
|
576
|
+
title: document.title
|
|
577
|
+
});
|
|
578
|
+
this.observer = new MutationObserver((mutations) => {
|
|
579
|
+
for (const m of mutations) {
|
|
580
|
+
if (m.type === "childList") {
|
|
581
|
+
this.pushEvent("mutation", {
|
|
582
|
+
target: this.describeEl(m.target),
|
|
583
|
+
added: m.addedNodes.length,
|
|
584
|
+
removed: m.removedNodes.length
|
|
585
|
+
});
|
|
586
|
+
} else if (m.type === "attributes") {
|
|
587
|
+
this.pushEvent("mutation", {
|
|
588
|
+
target: this.describeEl(m.target),
|
|
589
|
+
attr: m.attributeName
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
this.observer.observe(document.body, {
|
|
595
|
+
childList: true,
|
|
596
|
+
attributes: true,
|
|
597
|
+
subtree: true,
|
|
598
|
+
characterData: false
|
|
599
|
+
});
|
|
600
|
+
this.on("click", (e) => {
|
|
601
|
+
const target = e.target;
|
|
602
|
+
this.pushEvent("click", {
|
|
603
|
+
x: e.clientX,
|
|
604
|
+
y: e.clientY,
|
|
605
|
+
target: this.describeEl(target)
|
|
606
|
+
});
|
|
607
|
+
}, true);
|
|
608
|
+
let scrollTimeout = null;
|
|
609
|
+
this.on("scroll", () => {
|
|
610
|
+
if (scrollTimeout) return;
|
|
611
|
+
scrollTimeout = setTimeout(() => {
|
|
612
|
+
scrollTimeout = null;
|
|
613
|
+
this.pushEvent("scroll", {
|
|
614
|
+
x: window.scrollX,
|
|
615
|
+
y: window.scrollY
|
|
616
|
+
});
|
|
617
|
+
}, 250);
|
|
618
|
+
});
|
|
619
|
+
if (!config.blurInputs) {
|
|
620
|
+
this.on("input", (e) => {
|
|
621
|
+
const target = e.target;
|
|
622
|
+
if (this.shouldMask(target)) return;
|
|
623
|
+
this.pushEvent("input", {
|
|
624
|
+
target: this.describeEl(target),
|
|
625
|
+
length: target.value?.length || 0,
|
|
626
|
+
// Never send actual values - just metadata
|
|
627
|
+
type: target.type || "text"
|
|
628
|
+
});
|
|
629
|
+
}, true);
|
|
630
|
+
}
|
|
631
|
+
this.on("popstate", () => {
|
|
632
|
+
this.pushEvent("navigation", { url: location.pathname });
|
|
633
|
+
});
|
|
634
|
+
this.flushInterval = setInterval(() => this.flush(), 15e3);
|
|
635
|
+
this.stopTimer = setTimeout(() => {
|
|
636
|
+
this.client.log("Recording: max duration reached, stopping");
|
|
637
|
+
this.stop();
|
|
638
|
+
}, config.maxDuration * 1e3);
|
|
639
|
+
}
|
|
640
|
+
stop() {
|
|
641
|
+
if (this.observer) {
|
|
642
|
+
this.observer.disconnect();
|
|
643
|
+
this.observer = null;
|
|
644
|
+
}
|
|
645
|
+
for (const { event, fn } of this.handlers) {
|
|
646
|
+
(event === "click" ? document : window).removeEventListener(event, fn, true);
|
|
647
|
+
}
|
|
648
|
+
this.handlers = [];
|
|
649
|
+
if (this.flushInterval) clearInterval(this.flushInterval);
|
|
650
|
+
if (this.stopTimer) clearTimeout(this.stopTimer);
|
|
651
|
+
this.flush();
|
|
652
|
+
this.config = null;
|
|
653
|
+
}
|
|
654
|
+
// ── Private ──
|
|
655
|
+
on(event, fn, capture = false) {
|
|
656
|
+
const target = event === "click" ? document : window;
|
|
657
|
+
target.addEventListener(event, fn, capture);
|
|
658
|
+
this.handlers.push({ event, fn });
|
|
659
|
+
}
|
|
660
|
+
pushEvent(type, data) {
|
|
661
|
+
this.events.push({
|
|
662
|
+
type,
|
|
663
|
+
timestamp: Date.now() - this.startTime,
|
|
664
|
+
data
|
|
665
|
+
});
|
|
666
|
+
if (this.events.length >= 100) this.flush();
|
|
667
|
+
}
|
|
668
|
+
async flush() {
|
|
669
|
+
if (this.events.length === 0) return;
|
|
670
|
+
const batch = [...this.events];
|
|
671
|
+
this.events = [];
|
|
672
|
+
try {
|
|
673
|
+
await this.client.request("/ingest-data", {
|
|
674
|
+
type: "recording",
|
|
675
|
+
sessionId: this.sessionId,
|
|
676
|
+
events: batch
|
|
677
|
+
});
|
|
678
|
+
} catch (err) {
|
|
679
|
+
this.client.log(`Recording flush error: ${err}`);
|
|
680
|
+
this.events.unshift(...batch);
|
|
29
681
|
}
|
|
30
682
|
}
|
|
683
|
+
shouldMask(el) {
|
|
684
|
+
if (!this.config) return true;
|
|
685
|
+
for (const sel of this.config.maskSelectors) {
|
|
686
|
+
try {
|
|
687
|
+
if (el.matches(sel) || el.closest(sel)) return true;
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const type = el.type;
|
|
692
|
+
return type === "password" || type === "hidden";
|
|
693
|
+
}
|
|
694
|
+
describeEl(el) {
|
|
695
|
+
if (!el || !el.tagName) return "";
|
|
696
|
+
let desc = el.tagName.toLowerCase();
|
|
697
|
+
if (el.id) desc += `#${el.id}`;
|
|
698
|
+
const role = el.getAttribute("role");
|
|
699
|
+
if (role) desc += `[role=${role}]`;
|
|
700
|
+
const label = el.getAttribute("aria-label");
|
|
701
|
+
if (label) desc += `[${label.slice(0, 20)}]`;
|
|
702
|
+
return desc;
|
|
703
|
+
}
|
|
704
|
+
generateId() {
|
|
705
|
+
return "rec_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
706
|
+
}
|
|
31
707
|
};
|
|
32
708
|
|
|
33
709
|
// src/index.ts
|
|
34
710
|
var ZerocostSDK = class {
|
|
35
711
|
core;
|
|
712
|
+
ads;
|
|
713
|
+
track;
|
|
714
|
+
widget;
|
|
715
|
+
data;
|
|
716
|
+
recording;
|
|
36
717
|
constructor(config) {
|
|
37
718
|
this.core = new ZerocostClient(config);
|
|
719
|
+
this.ads = new AdsModule(this.core);
|
|
720
|
+
this.track = new TrackModule(this.core);
|
|
721
|
+
this.widget = new WidgetModule(this.core);
|
|
722
|
+
this.data = new LLMDataModule(this.core);
|
|
723
|
+
this.recording = new RecordingModule(this.core);
|
|
38
724
|
}
|
|
39
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Initialize the SDK. Automatically:
|
|
727
|
+
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
728
|
+
* 2. Starts LLM data collection if enabled
|
|
729
|
+
* 3. Starts UX session recording if enabled
|
|
730
|
+
*
|
|
731
|
+
* No custom components needed — ads render automatically.
|
|
732
|
+
* Enable `debug: true` in config to see detailed logs.
|
|
733
|
+
*/
|
|
734
|
+
async init() {
|
|
40
735
|
this.core.init();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
requestAd: (placementId) => {
|
|
45
|
-
this.core.log(`Requested ad for placement: ${placementId}`);
|
|
46
|
-
return Promise.resolve({ id: "ad_123", content: "test" });
|
|
736
|
+
if (typeof document === "undefined") {
|
|
737
|
+
this.core.log("Running in non-browser environment \u2014 skipping DOM injection.");
|
|
738
|
+
return;
|
|
47
739
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
740
|
+
this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
|
|
741
|
+
try {
|
|
742
|
+
const { display, dataCollection } = await this.core.request("/get-placements");
|
|
743
|
+
this.widget.autoInjectWithConfig(display);
|
|
744
|
+
if (dataCollection?.llm) {
|
|
745
|
+
this.data.start(dataCollection.llm);
|
|
746
|
+
}
|
|
747
|
+
if (dataCollection?.recording) {
|
|
748
|
+
this.recording.start(dataCollection.recording);
|
|
749
|
+
}
|
|
750
|
+
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
751
|
+
} catch (err) {
|
|
752
|
+
this.core.log(`Init error: ${err}. Attempting fallback ad injection...`);
|
|
753
|
+
this.widget.autoInject();
|
|
52
754
|
}
|
|
53
|
-
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Tear down all modules.
|
|
758
|
+
*/
|
|
759
|
+
destroy() {
|
|
760
|
+
this.widget.unmountAll();
|
|
761
|
+
this.data.stop();
|
|
762
|
+
this.recording.stop();
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Validate the configured API key against the server.
|
|
766
|
+
*/
|
|
767
|
+
async validateKey() {
|
|
768
|
+
try {
|
|
769
|
+
const result = await this.core.request("/validate-key");
|
|
770
|
+
return result;
|
|
771
|
+
} catch (err) {
|
|
772
|
+
return { valid: false, error: err.message };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
54
775
|
};
|
|
55
776
|
export {
|
|
777
|
+
LLMDataModule,
|
|
778
|
+
RecordingModule,
|
|
779
|
+
ZerocostClient,
|
|
56
780
|
ZerocostSDK
|
|
57
781
|
};
|