@sightspool/sdk 0.1.1 → 0.2.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/README.md +69 -15
- package/dist/index.cjs +326 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +175 -1
- package/dist/index.js.map +1 -1
- package/dist/sdk.global.js +21 -3
- package/dist/sdk.global.js.map +1 -1
- package/dist/survey-CICJQ5VI.js +145 -0
- package/dist/survey-CICJQ5VI.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ them).
|
|
|
14
14
|
> **not** act on your surface (surveys/nudges/experiments are Wave 0005, and every one
|
|
15
15
|
> is human-gated). Trigger sensitivity and intent inference calibrate with live traffic.
|
|
16
16
|
|
|
17
|
-
**Docs:** [full reference + CSP & framework guides](https://sightspool.
|
|
18
|
-
[llms.txt](https://sightspool.
|
|
17
|
+
**Docs:** [full reference + CSP & framework guides](https://sdk.sightspool.com/) ·
|
|
18
|
+
[llms.txt](https://sdk.sightspool.com/llms.txt) (the machine-readable install/CSP/config
|
|
19
19
|
doc for agents) · [npm](https://www.npmjs.com/package/@sightspool/sdk)
|
|
20
20
|
|
|
21
21
|
---
|
|
@@ -90,6 +90,7 @@ The script tag also reads these optional attributes (the no-build equivalent of
|
|
|
90
90
|
| `key` | `string` | — | **required.** Your publishable key (`pk_test_…` / `pk_live_…`), from the Connections → In-product SDK card. Publishable — safe to ship in client JS. See [Keys & environments](#keys--environments). |
|
|
91
91
|
| `endpoint` | `string` | the bundle's origin (script tag) / `https://app.sightspool.com` (npm) | Ingest base URL. The `<script>` install auto-resolves it to wherever `sdk.global.js` was served from (your app), so the key alone is enough; override for a CDN-hosted bundle or dev. |
|
|
92
92
|
| `boundaryAsk` | `boolean` | `true` | Show the one-tap "did you do what you came to do?" ask at session boundaries. |
|
|
93
|
+
| `interventions` | `boolean` | `true` | Show human-approved **surveys** served by your Sightspool workspace (see [Interventions](#interventions-surveys)). Set `false` to capture only. |
|
|
93
94
|
| `consent` | `boolean` | `true` | Start capturing immediately. Set `false` to stay paused until you call `Sightspool.consent(true)` (or `start()`) after obtaining consent. |
|
|
94
95
|
| `redact` | `string[]` | `[]` | CSS selectors whose captured text is **masked** (replaced with `‹redacted›`) before anything leaves the page. The event is still recorded — only its label is masked. |
|
|
95
96
|
| `block` | `string[]` | `[]` | CSS selectors whose events are **dropped entirely** (the hard opt-out). Equivalent to putting `data-sightspool-ignore` on the element. |
|
|
@@ -186,29 +187,44 @@ useEffect(() => {
|
|
|
186
187
|
> in the environment Next builds in. `strategy="afterInteractive"` keeps it off the critical
|
|
187
188
|
> path.
|
|
188
189
|
|
|
189
|
-
### React
|
|
190
|
+
### React / Next.js — `@sightspool/react`
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
For React apps, use the official bindings — a declarative provider over the core SDK:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm install @sightspool/react @sightspool/sdk react
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { SightspoolProvider } from '@sightspool/react'
|
|
200
|
+
|
|
201
|
+
<SightspoolProvider
|
|
202
|
+
apiKey={process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY!}
|
|
203
|
+
identity={user && { userId: user.id, account: user.account, plan: user.plan }}
|
|
204
|
+
>
|
|
205
|
+
<App />
|
|
206
|
+
</SightspoolProvider>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
It ships a `"use client"` banner (drops straight into a Next.js App Router **server**
|
|
210
|
+
layout), re-fires `identify` whenever `identity` changes, and takes a reactive `consent`
|
|
211
|
+
prop for cookie banners. Full API:
|
|
212
|
+
[@sightspool/react](https://www.npmjs.com/package/@sightspool/react) ·
|
|
213
|
+
[packages/react](https://github.com/sightspool/sdk/tree/main/packages/react).
|
|
214
|
+
|
|
215
|
+
<details><summary>Or wire the core SDK by hand (no wrapper)</summary>
|
|
193
216
|
|
|
194
217
|
```tsx
|
|
195
218
|
import { useEffect } from 'react'
|
|
196
219
|
import Sightspool from '@sightspool/sdk'
|
|
197
220
|
|
|
198
221
|
export function SightspoolBoot({ userId, account, plan }) {
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
|
|
201
|
-
}, [])
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
if (userId) Sightspool.identify(userId, { account, plan })
|
|
204
|
-
}, [userId, account, plan])
|
|
222
|
+
useEffect(() => { Sightspool.init({ key: import.meta.env.VITE_SIGHTSPOOL_KEY }) }, [])
|
|
223
|
+
useEffect(() => { if (userId) Sightspool.identify(userId, { account, plan }) }, [userId, account, plan])
|
|
205
224
|
return null
|
|
206
225
|
}
|
|
207
226
|
```
|
|
208
|
-
|
|
209
|
-
> A first-class **`@sightspool/react`** wrapper (a `<SightspoolProvider>` + a `useSightspool`
|
|
210
|
-
> hook over `init`/`identify`) is planned —
|
|
211
|
-
> [issue #3](https://github.com/sightspool/sdk/issues/3).
|
|
227
|
+
</details>
|
|
212
228
|
|
|
213
229
|
---
|
|
214
230
|
|
|
@@ -245,6 +261,44 @@ If you pass a custom `endpoint`, use *that* origin in `connect-src`. The beacon
|
|
|
245
261
|
|
|
246
262
|
---
|
|
247
263
|
|
|
264
|
+
## Interventions (surveys)
|
|
265
|
+
|
|
266
|
+
The SDK is two-way. Besides *capturing*, it can show a **human-approved survey** at the
|
|
267
|
+
moment of friction — the "ask" side of Sightspool. You don't author these in code: your
|
|
268
|
+
team proposes a survey off a proven finding in the app, **a human approves it**, sets who
|
|
269
|
+
sees it (route / account / plan), and the SDK serves it. Nothing reaches a user without
|
|
270
|
+
that approval ("no proof **and** no approval, no act").
|
|
271
|
+
|
|
272
|
+
It's on by default — the same one or two install lines that capture also serve. When the
|
|
273
|
+
SDK loads (and on each route change) it asks your workspace *"anything to show this user
|
|
274
|
+
here?"*; if there's a matching approved survey, it renders a small card in a shadow root
|
|
275
|
+
(your CSS can't reach it; it leaks no styles), and posts the answer back.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
Sightspool.init({ key: "pk_live_…" }); // serving is on by default
|
|
279
|
+
Sightspool.init({ key: "pk_live_…", interventions: false }); // capture only
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```html
|
|
283
|
+
<!-- script-tag install: opt out with one attribute -->
|
|
284
|
+
<script src="https://app.sightspool.com/sdk.global.js"
|
|
285
|
+
data-sightspool-key="pk_live_…"
|
|
286
|
+
data-sightspool-no-interventions></script>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**What shows, and how often** — all server-gated, so you stay in control:
|
|
290
|
+
|
|
291
|
+
- **Targeting** — only to the route / account / plan the approver chose.
|
|
292
|
+
- **De-dup** — a survey a user has answered never reappears (a per-user key is kept
|
|
293
|
+
locally; it's anonymous unless you've called `identify()`).
|
|
294
|
+
- **Won't pester** — at most one survey on screen, one per session, and it shares a
|
|
295
|
+
cooldown with the boundary ask so the two never stack back-to-back. The run also stops
|
|
296
|
+
itself once it hits the approver's response target.
|
|
297
|
+
|
|
298
|
+
The survey widget is **lazily loaded** — it splits into its own chunk and adds nothing to
|
|
299
|
+
your bundle until a survey is actually served. Today the SDK renders one-tap / short-text
|
|
300
|
+
surveys; richer types (incl. voice micro-research) render as they ship.
|
|
301
|
+
|
|
248
302
|
## What it captures
|
|
249
303
|
|
|
250
304
|
- **Passively, no wiring** — route/screen sequence, clicks, **dead-clicks** and
|
package/dist/index.cjs
CHANGED
|
@@ -199,6 +199,157 @@ input { font: inherit; font-size: 13px; width: 100%; box-sizing: border-box; pad
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
// src/survey.ts
|
|
203
|
+
var survey_exports = {};
|
|
204
|
+
__export(survey_exports, {
|
|
205
|
+
showSurvey: () => showSurvey
|
|
206
|
+
});
|
|
207
|
+
function showSurvey(config) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
if (typeof document === "undefined") {
|
|
210
|
+
resolve({ dismissed: true });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let settled = false;
|
|
214
|
+
const host = document.createElement("div");
|
|
215
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
216
|
+
function teardown() {
|
|
217
|
+
try {
|
|
218
|
+
host.remove();
|
|
219
|
+
} catch (e) {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function finish(result, thank) {
|
|
223
|
+
if (settled) return;
|
|
224
|
+
settled = true;
|
|
225
|
+
resolve(result);
|
|
226
|
+
if (thank) {
|
|
227
|
+
thankYou();
|
|
228
|
+
setTimeout(teardown, THANKS_MS);
|
|
229
|
+
} else {
|
|
230
|
+
teardown();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const style = document.createElement("style");
|
|
234
|
+
style.textContent = STYLE2;
|
|
235
|
+
shadow.appendChild(style);
|
|
236
|
+
const wrap = document.createElement("div");
|
|
237
|
+
wrap.className = "wrap";
|
|
238
|
+
shadow.appendChild(wrap);
|
|
239
|
+
function card() {
|
|
240
|
+
wrap.innerHTML = "";
|
|
241
|
+
const c = document.createElement("div");
|
|
242
|
+
c.className = "card";
|
|
243
|
+
wrap.appendChild(c);
|
|
244
|
+
return c;
|
|
245
|
+
}
|
|
246
|
+
function withClose(c) {
|
|
247
|
+
const x = document.createElement("button");
|
|
248
|
+
x.className = "x";
|
|
249
|
+
x.textContent = "\xD7";
|
|
250
|
+
x.setAttribute("aria-label", "Dismiss");
|
|
251
|
+
x.onclick = () => finish({ dismissed: true }, false);
|
|
252
|
+
c.appendChild(x);
|
|
253
|
+
}
|
|
254
|
+
function question() {
|
|
255
|
+
var _a2, _b, _c;
|
|
256
|
+
const c = card();
|
|
257
|
+
withClose(c);
|
|
258
|
+
const q = document.createElement("p");
|
|
259
|
+
q.className = "q";
|
|
260
|
+
q.textContent = config.question;
|
|
261
|
+
c.appendChild(q);
|
|
262
|
+
for (const opt of ((_a2 = config.options) != null ? _a2 : []).slice(0, 6)) {
|
|
263
|
+
const b = document.createElement("button");
|
|
264
|
+
b.className = "opt";
|
|
265
|
+
b.textContent = opt;
|
|
266
|
+
b.onclick = () => finish({ choice: opt }, true);
|
|
267
|
+
c.appendChild(b);
|
|
268
|
+
}
|
|
269
|
+
if (config.allow_text) {
|
|
270
|
+
const se = document.createElement("button");
|
|
271
|
+
se.className = "opt";
|
|
272
|
+
se.textContent = ((_c = (_b = config.options) == null ? void 0 : _b.length) != null ? _c : 0) > 0 ? "Something else\u2026" : "Answer\u2026";
|
|
273
|
+
se.onclick = freeText;
|
|
274
|
+
c.appendChild(se);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function freeText() {
|
|
278
|
+
const c = card();
|
|
279
|
+
withClose(c);
|
|
280
|
+
const q = document.createElement("p");
|
|
281
|
+
q.className = "q";
|
|
282
|
+
q.textContent = config.question;
|
|
283
|
+
c.appendChild(q);
|
|
284
|
+
const input = document.createElement("input");
|
|
285
|
+
input.type = "text";
|
|
286
|
+
input.placeholder = "In your own words\u2026";
|
|
287
|
+
c.appendChild(input);
|
|
288
|
+
const row = document.createElement("div");
|
|
289
|
+
row.className = "row";
|
|
290
|
+
const send = document.createElement("button");
|
|
291
|
+
send.className = "primary";
|
|
292
|
+
send.textContent = "Send";
|
|
293
|
+
const submit = () => {
|
|
294
|
+
const text = input.value.trim();
|
|
295
|
+
if (!text) {
|
|
296
|
+
finish({ dismissed: true }, false);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
finish({ text }, true);
|
|
300
|
+
};
|
|
301
|
+
send.onclick = submit;
|
|
302
|
+
input.onkeydown = (e) => {
|
|
303
|
+
if (e.key === "Enter") submit();
|
|
304
|
+
};
|
|
305
|
+
row.appendChild(send);
|
|
306
|
+
c.appendChild(row);
|
|
307
|
+
try {
|
|
308
|
+
input.focus();
|
|
309
|
+
} catch (e) {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function thankYou() {
|
|
313
|
+
const c = card();
|
|
314
|
+
const p = document.createElement("p");
|
|
315
|
+
p.className = "done";
|
|
316
|
+
p.textContent = "Thanks \u2014 that helps.";
|
|
317
|
+
c.appendChild(p);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
document.body.appendChild(host);
|
|
321
|
+
question();
|
|
322
|
+
} catch (e) {
|
|
323
|
+
finish({ dismissed: true }, false);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
var STYLE2, THANKS_MS;
|
|
328
|
+
var init_survey = __esm({
|
|
329
|
+
"src/survey.ts"() {
|
|
330
|
+
STYLE2 = `
|
|
331
|
+
:host { all: initial; }
|
|
332
|
+
.wrap { position: fixed; bottom: 20px; right: 20px; z-index: 2147483000;
|
|
333
|
+
width: 340px; max-width: calc(100vw - 32px); font-family: system-ui, -apple-system, sans-serif; }
|
|
334
|
+
.card { position: relative; background: #fff; color: #18181b; border: 1px solid #e4e4e7;
|
|
335
|
+
border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.12); padding: 16px; }
|
|
336
|
+
.q { font-size: 14px; font-weight: 600; margin: 0 0 12px; line-height: 1.35; padding-right: 16px; }
|
|
337
|
+
.row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
338
|
+
button { font: inherit; font-size: 13px; cursor: pointer; border-radius: 8px; padding: 8px 12px;
|
|
339
|
+
border: 1px solid #e4e4e7; background: #fafafa; color: #18181b; }
|
|
340
|
+
button:hover { background: #f4f4f5; }
|
|
341
|
+
button.primary { background: #18181b; color: #fff; border-color: #18181b; }
|
|
342
|
+
.opt { display: block; width: 100%; text-align: left; margin-bottom: 6px; }
|
|
343
|
+
.x { position: absolute; top: 8px; right: 10px; border: none; background: none; font-size: 16px;
|
|
344
|
+
color: #a1a1aa; padding: 2px 6px; cursor: pointer; }
|
|
345
|
+
.done { font-size: 13px; color: #18181b; margin: 0; }
|
|
346
|
+
input { font: inherit; font-size: 13px; width: 100%; box-sizing: border-box; padding: 8px 10px;
|
|
347
|
+
border: 1px solid #e4e4e7; border-radius: 8px; margin-bottom: 8px; }
|
|
348
|
+
`;
|
|
349
|
+
THANKS_MS = 1100;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
202
353
|
// src/capture.ts
|
|
203
354
|
init_privacy();
|
|
204
355
|
var TRAIL_MAX = 30;
|
|
@@ -573,6 +724,117 @@ function canPrompt(state, now, caps = DEFAULT_CAPS) {
|
|
|
573
724
|
return true;
|
|
574
725
|
}
|
|
575
726
|
|
|
727
|
+
// src/intervention.ts
|
|
728
|
+
var RENDERABLE = /* @__PURE__ */ new Set(["survey"]);
|
|
729
|
+
function str(v, max) {
|
|
730
|
+
if (typeof v !== "string") return void 0;
|
|
731
|
+
const t = v.trim();
|
|
732
|
+
return t ? t.slice(0, max) : void 0;
|
|
733
|
+
}
|
|
734
|
+
function buildServeContext(route, identity) {
|
|
735
|
+
const ctx = {};
|
|
736
|
+
const r = str(route, 500);
|
|
737
|
+
if (r) ctx.route = r;
|
|
738
|
+
const account = str(identity == null ? void 0 : identity.account, 200);
|
|
739
|
+
if (account) ctx.account = account;
|
|
740
|
+
const plan = str(identity == null ? void 0 : identity.plan, 80);
|
|
741
|
+
if (plan) ctx.plan = plan;
|
|
742
|
+
return ctx;
|
|
743
|
+
}
|
|
744
|
+
function normalizeServed(raw) {
|
|
745
|
+
if (!raw || typeof raw !== "object") return null;
|
|
746
|
+
const r = raw;
|
|
747
|
+
const id = str(r.id, 64);
|
|
748
|
+
const type = str(r.type, 40);
|
|
749
|
+
if (!id || !type || !RENDERABLE.has(type)) return null;
|
|
750
|
+
const cfg = r.config && typeof r.config === "object" ? r.config : {};
|
|
751
|
+
const question = str(cfg.question, 500);
|
|
752
|
+
if (!question) return null;
|
|
753
|
+
const options = Array.isArray(cfg.options) ? cfg.options.map((o) => str(o, 200)).filter((o) => !!o).slice(0, 6) : void 0;
|
|
754
|
+
const config = {
|
|
755
|
+
question,
|
|
756
|
+
...options && options.length ? { options } : {},
|
|
757
|
+
allow_text: cfg.allow_text === true
|
|
758
|
+
};
|
|
759
|
+
return { id, type, config };
|
|
760
|
+
}
|
|
761
|
+
function pickRespondentKey(userId, persisted, sessionRef) {
|
|
762
|
+
const uid = str(userId, 200);
|
|
763
|
+
if (uid) return uid;
|
|
764
|
+
const p = str(persisted, 200);
|
|
765
|
+
if (p) return p;
|
|
766
|
+
return sessionRef;
|
|
767
|
+
}
|
|
768
|
+
var RK_STORAGE_KEY = "sightspool_rk";
|
|
769
|
+
function getRespondentKey(identity, sessionRef) {
|
|
770
|
+
const uid = str(identity == null ? void 0 : identity.userId, 200);
|
|
771
|
+
if (uid) return uid;
|
|
772
|
+
try {
|
|
773
|
+
let v = localStorage.getItem(RK_STORAGE_KEY);
|
|
774
|
+
if (!v) {
|
|
775
|
+
v = "anon-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
776
|
+
localStorage.setItem(RK_STORAGE_KEY, v);
|
|
777
|
+
}
|
|
778
|
+
return pickRespondentKey(void 0, v, sessionRef);
|
|
779
|
+
} catch (e) {
|
|
780
|
+
return sessionRef;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function serveUrl(endpoint, path) {
|
|
784
|
+
return `${endpoint.replace(/\/+$/, "")}/api/sdk/${path}`;
|
|
785
|
+
}
|
|
786
|
+
async function fetchIntervention(opts) {
|
|
787
|
+
var _a2;
|
|
788
|
+
try {
|
|
789
|
+
if (typeof fetch !== "function") return null;
|
|
790
|
+
const res = await fetch(serveUrl(opts.endpoint, "serve"), {
|
|
791
|
+
method: "POST",
|
|
792
|
+
headers: { "Content-Type": "text/plain" },
|
|
793
|
+
body: JSON.stringify({
|
|
794
|
+
key: opts.key,
|
|
795
|
+
context: opts.ctx,
|
|
796
|
+
respondent_key: opts.respondentKey
|
|
797
|
+
}),
|
|
798
|
+
mode: "cors",
|
|
799
|
+
credentials: "omit"
|
|
800
|
+
});
|
|
801
|
+
if (!res.ok) return null;
|
|
802
|
+
const data = await res.json();
|
|
803
|
+
const served = normalizeServed(data == null ? void 0 : data.intervention);
|
|
804
|
+
if (opts.debug) console.debug("[sightspool] serve", (_a2 = served == null ? void 0 : served.id) != null ? _a2 : "(none)");
|
|
805
|
+
return served;
|
|
806
|
+
} catch (e) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function submitResponse(opts) {
|
|
811
|
+
try {
|
|
812
|
+
const url = serveUrl(opts.endpoint, "respond");
|
|
813
|
+
const json = JSON.stringify({
|
|
814
|
+
key: opts.key,
|
|
815
|
+
intervention_id: opts.interventionId,
|
|
816
|
+
response: opts.response,
|
|
817
|
+
respondent_key: opts.respondentKey
|
|
818
|
+
});
|
|
819
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
820
|
+
const blob = new Blob([json], { type: "text/plain" });
|
|
821
|
+
if (navigator.sendBeacon(url, blob)) return;
|
|
822
|
+
}
|
|
823
|
+
if (typeof fetch === "function") {
|
|
824
|
+
void fetch(url, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
headers: { "Content-Type": "text/plain" },
|
|
827
|
+
body: json,
|
|
828
|
+
keepalive: true,
|
|
829
|
+
mode: "cors",
|
|
830
|
+
credentials: "omit"
|
|
831
|
+
}).catch(() => {
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
} catch (e) {
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
576
838
|
// src/env.ts
|
|
577
839
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", ""]);
|
|
578
840
|
function isLocalhost(hostname) {
|
|
@@ -585,6 +847,8 @@ function isLocalhost(hostname) {
|
|
|
585
847
|
var DEFAULT_ENDPOINT = "https://app.sightspool.com";
|
|
586
848
|
var FRICTION_EMIT_COOLDOWN_MS = 3e4;
|
|
587
849
|
var EVENT_DEBOUNCE_MS = 600;
|
|
850
|
+
var SERVE_SETTLE_MS = 2500;
|
|
851
|
+
var MAX_INTERVENTIONS_PER_SESSION = 1;
|
|
588
852
|
function currentHostname() {
|
|
589
853
|
try {
|
|
590
854
|
return typeof location !== "undefined" ? location.hostname : "";
|
|
@@ -649,9 +913,53 @@ async function maybePrompt(c, trigger) {
|
|
|
649
913
|
c.egress.enqueue(buildSignal(c, trigger));
|
|
650
914
|
}
|
|
651
915
|
}
|
|
916
|
+
async function maybeServeIntervention(c) {
|
|
917
|
+
if (!c.running || !c.interventionsEnabled || c.suppressed) return;
|
|
918
|
+
if (c.surveyOnScreen) return;
|
|
919
|
+
if (c.interventionsShown >= MAX_INTERVENTIONS_PER_SESSION) return;
|
|
920
|
+
const now = Date.now();
|
|
921
|
+
if (c.fatigue.lastPromptAt !== null && now - c.fatigue.lastPromptAt < DEFAULT_CAPS.cooldownMs)
|
|
922
|
+
return;
|
|
923
|
+
const respondentKey = getRespondentKey(c.identity, c.sessionRef);
|
|
924
|
+
const served = await fetchIntervention({
|
|
925
|
+
endpoint: c.config.endpoint,
|
|
926
|
+
key: c.config.key,
|
|
927
|
+
ctx: buildServeContext(c.capture.route(), c.identity),
|
|
928
|
+
respondentKey,
|
|
929
|
+
debug: c.config.debug
|
|
930
|
+
});
|
|
931
|
+
if (!served || !c.running) return;
|
|
932
|
+
c.surveyOnScreen = true;
|
|
933
|
+
c.interventionsShown += 1;
|
|
934
|
+
c.fatigue.lastPromptAt = Date.now();
|
|
935
|
+
try {
|
|
936
|
+
const { showSurvey: showSurvey2 } = await Promise.resolve().then(() => (init_survey(), survey_exports));
|
|
937
|
+
const result = await showSurvey2(served.config);
|
|
938
|
+
if (!result.dismissed && (result.choice || result.text)) {
|
|
939
|
+
submitResponse({
|
|
940
|
+
endpoint: c.config.endpoint,
|
|
941
|
+
key: c.config.key,
|
|
942
|
+
interventionId: served.id,
|
|
943
|
+
response: { choice: result.choice, text: result.text },
|
|
944
|
+
respondentKey
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
} catch (e) {
|
|
948
|
+
} finally {
|
|
949
|
+
c.surveyOnScreen = false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
652
952
|
function onCaptureEvent() {
|
|
653
953
|
const c = ctrl;
|
|
654
954
|
if (!c || !c.running) return;
|
|
955
|
+
try {
|
|
956
|
+
const route = c.capture.route();
|
|
957
|
+
if (route && route !== c.lastServeRoute) {
|
|
958
|
+
c.lastServeRoute = route;
|
|
959
|
+
void maybeServeIntervention(c);
|
|
960
|
+
}
|
|
961
|
+
} catch (e) {
|
|
962
|
+
}
|
|
655
963
|
if (c.eventTimer !== null) return;
|
|
656
964
|
c.eventTimer = (typeof window !== "undefined" ? window.setTimeout : setTimeout)(() => {
|
|
657
965
|
c.eventTimer = null;
|
|
@@ -692,6 +1000,18 @@ function startRuntime(c) {
|
|
|
692
1000
|
document.addEventListener("mouseout", onExitIntent);
|
|
693
1001
|
} catch (e) {
|
|
694
1002
|
}
|
|
1003
|
+
if (c.interventionsEnabled) {
|
|
1004
|
+
c.lastServeRoute = (() => {
|
|
1005
|
+
try {
|
|
1006
|
+
return c.capture.route();
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
})();
|
|
1011
|
+
(typeof window !== "undefined" ? window.setTimeout : setTimeout)(() => {
|
|
1012
|
+
void maybeServeIntervention(c);
|
|
1013
|
+
}, SERVE_SETTLE_MS);
|
|
1014
|
+
}
|
|
695
1015
|
if (c.config.debug) console.debug("[sightspool] started", c.sessionRef);
|
|
696
1016
|
}
|
|
697
1017
|
function init(config) {
|
|
@@ -723,7 +1043,11 @@ function init(config) {
|
|
|
723
1043
|
lastFrictionEmitAt: 0,
|
|
724
1044
|
eventTimer: null,
|
|
725
1045
|
running: false,
|
|
726
|
-
suppressed: isLocalhost(currentHostname()) && config.captureOnLocalhost !== true
|
|
1046
|
+
suppressed: isLocalhost(currentHostname()) && config.captureOnLocalhost !== true,
|
|
1047
|
+
interventionsEnabled: config.interventions !== false,
|
|
1048
|
+
lastServeRoute: null,
|
|
1049
|
+
surveyOnScreen: false,
|
|
1050
|
+
interventionsShown: 0
|
|
727
1051
|
};
|
|
728
1052
|
if (config.consent !== false) startRuntime(ctrl);
|
|
729
1053
|
} catch (err) {
|
|
@@ -796,6 +1120,7 @@ try {
|
|
|
796
1120
|
redact: list("data-sightspool-redact"),
|
|
797
1121
|
block: list("data-sightspool-block"),
|
|
798
1122
|
captureOnLocalhost: (el == null ? void 0 : el.hasAttribute("data-sightspool-capture-localhost")) || void 0,
|
|
1123
|
+
interventions: (el == null ? void 0 : el.hasAttribute("data-sightspool-no-interventions")) ? false : void 0,
|
|
799
1124
|
debug: (el == null ? void 0 : el.hasAttribute("data-sightspool-debug")) || void 0
|
|
800
1125
|
});
|
|
801
1126
|
}
|