Targeted surveys
Open People → Surveys in the application environment you want to use. Create an NPS, churn-reason or open-text draft, review its question and audience, then launch it. People and Analytics must both be enabled. Operators need people.read to view results and people.write plus a passkey verified within ten minutes to create or change collection. Written responses additionally require people.pii.read; they are omitted from the database projection otherwise.
NPS accepts a score from 0 through 10 and an optional comment. The displayed NPS is the percentage of promoters (9–10) minus the percentage of detractors (0–6). Churn surveys accept one configured reason and an optional comment. Open-text surveys require a comment. Comments are limited to 2,000 characters. These are self-reported answers from responding visitors, not a representative sample of every customer.
Browser installation
The optional entry point does not add code to the base tracker or its script-tag bundle. It is safe to import during server rendering; create and show the widget in a browser lifecycle effect or event handler. Node SDK ingestion remains compatible and can send the events used for targeting when it preserves the browser's anonymous ID.
import { createClient } from "@saaspro/browser";
import { createSurveyWidget } from "@saaspro/browser/surveys";
const client = createClient().init({
key: "YOUR_PUBLIC_INGEST_KEY",
host: "https://saaspro.dev",
consent: "pending",
});
const surveys = createSurveyWidget(client, {
consent: "pending",
onError: error => console.warn(error.message),
});
// Invoke after the visitor chooses both preferences in your consent UI.
client.consent("granted");
surveys.setConsent("granted");
// Invoke at an appropriate moment in your application.
client.track("onboarding_completed");
await surveys.show("onboarding-feedback");
// Wire these to your application's persistent privacy controls.
surveys.optOut();
surveys.setConsent("granted");
// Invoke when the owning component unmounts.
surveys.destroy();
Keep the consent and opt-out calls in the relevant user handlers, not together in a startup sequence. show() flushes the client's queued events before checking server eligibility. Without a key argument, it selects the oldest launched matching survey. It returns true if a widget was displayed; false means no eligible survey, no consent, a request already in progress, or a failed request. onError reports transport failures. No automatic polling runs and show() does not create a target event itself.
By default the widget appears at the lower right. Supply container: HTMLElement to render inline. The widget uses a closed shadow root, native form controls, a wrapping 0–10 score scale, visible focus, light/dark styles and an optional nonce for browsers needing its stylesheet fallback under CSP. Allow the configured host in the application's connect-src. Custom script-tag-only installations can import this separate package through their build system.
Consent and preferences
Survey consent defaults to pending and is separate from analytics consent. The widget requires granted, persistent analytics collection and a public ingest key. It stays off in ephemeral mode, while analytics consent is pending/revoked, and after analytics opt-out. Reset, session changes, identity changes and consent revocation discard pending requests and close the widget. Survey opt-out is saved in browser local storage for this host/key; saved opt-out wins over an initial consent: "granted" until the user explicitly calls setConsent("granted"). A preference write failure leaves the widget off in memory and calls onError. Answers and invitation tokens are never persisted by the widget.
The widget has a Don't show surveys again control. Keep an accessible survey preference in the host application's settings so visitors can later opt back in. Browser storage deletion and use of another browser reset this local preference. Consent in the request is an SDK collection contract, not proof of a legal consent record or authenticated end-user identity.
Targeting and collection
An optional event condition requires an accepted track event with the same anonymous ID in the selected environment, within the configured 1–30 days (also capped by retention). If the request supplies a distinct ID, the event must match it too. Dropped, sampled or unflushed events do not qualify. Optional flag targeting evaluates the named flag on the server using the supplied SDK identities, in the selected environment. It accepts boolean values or an exact variant key; client-supplied flag results are never trusted. This identity-only evaluation supplies no custom properties. Normal missing-property semantics still apply: for example, a not-equals rule can match an absent property. Review the target flag before launch. When event and flag conditions are both configured, both must match. Flags access is required to configure a flag target.
An eligible visitor receives only the selected public question, choices and a 30-minute response capability. There is no public list, response read, identity lookup or aggregate endpoint. The server derives organization, application and environment from the ingest key and enforces its configured origins, module state, body limits and rate limits. Management tokens are refused. Raw anonymous and distinct IDs are not stored in survey tables; per-survey HMACs support suppression. Public SDK identities and responses are spoofable and must never authorize account changes.
One invitation may produce one answer or one dismissal. Exact retries are acknowledged, while changed retries conflict. A lost invitation can be reissued after thirty minutes. A response or dismissal suppresses that survey for the anonymous browser identity during retention. Invitations issued counts retained identities and does not claim a verified impression. A 100-survey lifetime limit and 10 active surveys apply per environment; at most 50,000 invitations are retained and 5,000 offers (including reissues) can be issued per UTC day. The API also uses a per-key, per-instance request limiter.
Questions and targeting become immutable after launch. Pause immediately refuses new invitations and responses; resume retains the same design. Archive ends collection permanently. Delete is available for drafts, paused or archived surveys and removes invitations and answers permanently. The reserved key and audit record remain. Every collection and administration mutation appends an audit event; audit payloads omit answers, invitation tokens and raw respondent IDs.
Retention and transport
Results, responses and suppression are retained for 90 days or the application's shorter retention. Reads filter expired evidence immediately. The surveys.maintain worker removes expired rows in bounded batches every five minutes; physical removal depends on the worker running. Daily aggregate issuance budgets are kept for 90 days. After suppression expires and is removed, a visitor can qualify again.
POST /api/v1/surveys accepts a public or secret ingest key in the JSON body (key), Authorization: Bearer, or x-spm-key. The browser sends JSON as text/plain, without credentials. Bodies are bounded to 12 KiB. Every request includes consent: true.
- Offer:
{ action: "offer", anonymousId, distinctId?, survey? }→{ survey: null | { id, key, template, question, choices, thanks, deliveryId, token } }. - Respond:
{ action: "respond", deliveryId, token, score?, choice?, text? }→{ accepted: true }. - Dismiss:
{ action: "dismiss", deliveryId, token }→{ accepted: true }.
Keep capabilities in memory, retry the exact same response after a transport failure, and discard them on consent or identity changes. An expired, paused or completed invitation may refuse a new write. The SDK keeps the form available for retry and reports the error; it never treats an HTTP failure as a recorded response.