@sightspool/sdk 0.1.0 → 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 +193 -1
- 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 +9 -7
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ 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://sdk.sightspool.com/) ·
|
|
18
|
+
[llms.txt](https://sdk.sightspool.com/llms.txt) (the machine-readable install/CSP/config
|
|
19
|
+
doc for agents) · [npm](https://www.npmjs.com/package/@sightspool/sdk)
|
|
20
|
+
|
|
17
21
|
---
|
|
18
22
|
|
|
19
23
|
## Install — two lines
|
|
@@ -83,9 +87,10 @@ The script tag also reads these optional attributes (the no-build equivalent of
|
|
|
83
87
|
|
|
84
88
|
| option | type | default | purpose |
|
|
85
89
|
|---|---|---|---|
|
|
86
|
-
| `key` | `string` | — | **required.** Your publishable key (`pk_live_…`), from the Connections → In-product SDK card. Publishable — safe to ship in client JS. |
|
|
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). |
|
|
87
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. |
|
|
88
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. |
|
|
89
94
|
| `consent` | `boolean` | `true` | Start capturing immediately. Set `false` to stay paused until you call `Sightspool.consent(true)` (or `start()`) after obtaining consent. |
|
|
90
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. |
|
|
91
96
|
| `block` | `string[]` | `[]` | CSS selectors whose events are **dropped entirely** (the hard opt-out). Equivalent to putting `data-sightspool-ignore` on the element. |
|
|
@@ -107,6 +112,193 @@ un-allowlisted origin.
|
|
|
107
112
|
|
|
108
113
|
---
|
|
109
114
|
|
|
115
|
+
## Keys & environments
|
|
116
|
+
|
|
117
|
+
Your key is **publishable** — safe to ship in client JS (the Stripe `pk_` model). Two
|
|
118
|
+
prefixes, one per environment:
|
|
119
|
+
|
|
120
|
+
| prefix | use it for |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `pk_test_…` | development / staging / preview deploys |
|
|
123
|
+
| `pk_live_…` | production |
|
|
124
|
+
|
|
125
|
+
The SDK treats both prefixes **identically** — there's no client-side special-casing; the
|
|
126
|
+
prefix tells *Sightspool* (at ingest) which environment a Signal belongs to, so test traffic
|
|
127
|
+
never mixes into production analytics. Issue both from **Connections → In-product SDK**.
|
|
128
|
+
|
|
129
|
+
> The SDK also **no-ops on localhost** by default (see `captureOnLocalhost`), so even a
|
|
130
|
+
> `pk_live_` key won't capture from `npm run dev`. Test keys are for *deployed* non-prod
|
|
131
|
+
> environments (staging, previews).
|
|
132
|
+
|
|
133
|
+
Keep the key in an environment variable rather than hardcoding it, and pick test vs live by
|
|
134
|
+
environment. The key is exposed to the browser, so use your framework's **client** env-var
|
|
135
|
+
prefix (`NEXT_PUBLIC_`, `VITE_`, `PUBLIC_`, …) — it's publishable, so that's expected:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
Sightspool.init({ key: process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY })
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# .env.development / preview
|
|
143
|
+
NEXT_PUBLIC_SIGHTSPOOL_KEY=pk_test_…
|
|
144
|
+
# .env.production
|
|
145
|
+
NEXT_PUBLIC_SIGHTSPOOL_KEY=pk_live_…
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Framework integration
|
|
151
|
+
|
|
152
|
+
### Next.js (App Router) — `next/script`
|
|
153
|
+
|
|
154
|
+
The idiomatic install is `next/script`, not a raw `<script>`. Add it once in your root
|
|
155
|
+
layout — the tag auto-`init`s from `data-sightspool-key`:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
// app/layout.tsx
|
|
159
|
+
import Script from 'next/script'
|
|
160
|
+
|
|
161
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
162
|
+
return (
|
|
163
|
+
<html lang="en">
|
|
164
|
+
<body>
|
|
165
|
+
{children}
|
|
166
|
+
<Script
|
|
167
|
+
src="https://app.sightspool.com/sdk.global.js"
|
|
168
|
+
data-sightspool-key={process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY}
|
|
169
|
+
strategy="afterInteractive"
|
|
170
|
+
/>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Then `identify` the user once known (e.g. a client component after auth):
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
'use client'
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
window.Sightspool?.identify(user.id, { account: user.account, plan: user.plan })
|
|
183
|
+
}, [user])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
> `data-sightspool-key` is inlined at build time, so `NEXT_PUBLIC_SIGHTSPOOL_KEY` must be set
|
|
187
|
+
> in the environment Next builds in. `strategy="afterInteractive"` keeps it off the critical
|
|
188
|
+
> path.
|
|
189
|
+
|
|
190
|
+
### React / Next.js — `@sightspool/react`
|
|
191
|
+
|
|
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>
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { useEffect } from 'react'
|
|
219
|
+
import Sightspool from '@sightspool/sdk'
|
|
220
|
+
|
|
221
|
+
export function SightspoolBoot({ 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])
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
</details>
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Content-Security-Policy
|
|
232
|
+
|
|
233
|
+
If your app sets a CSP, allow the SDK's two footprints — the **script load** and the
|
|
234
|
+
**ingest beacon**. With the standard install they're the *same host* (the bundle is served
|
|
235
|
+
from the app it ingests to), so it's one host in two directives:
|
|
236
|
+
|
|
237
|
+
**Script-tag install**
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
script-src https://app.sightspool.com;
|
|
241
|
+
connect-src https://app.sightspool.com;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**npm / bundler install** — the SDK is bundled into your own first-party JS, so no
|
|
245
|
+
`script-src` host is needed; only the ingest origin:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
connect-src https://app.sightspool.com;
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
If you pass a custom `endpoint`, use *that* origin in `connect-src`. The beacon goes via
|
|
252
|
+
`navigator.sendBeacon` with a `fetch(keepalive)` fallback — both governed by `connect-src`.
|
|
253
|
+
|
|
254
|
+
- **`strict-dynamic` / nonce.** Under `script-src 'strict-dynamic'`, host allowlists are
|
|
255
|
+
ignored for scripts — give the `<script>` tag your per-request nonce (`nonce={nonce}` in
|
|
256
|
+
Next) so it's trusted. `connect-src` still needs the ingest host.
|
|
257
|
+
- **Prompt styles.** The one-tap prompt renders into a **shadow root** and injects its own
|
|
258
|
+
`<style>`. Under a strict `style-src` without `'unsafe-inline'`, those styles may not apply
|
|
259
|
+
— the prompt stays **fully functional but unstyled** (the SDK never throws into your page).
|
|
260
|
+
Add `'unsafe-inline'` to `style-src` if you want it styled.
|
|
261
|
+
|
|
262
|
+
---
|
|
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
|
+
|
|
110
302
|
## What it captures
|
|
111
303
|
|
|
112
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
|
}
|