@lumianalytics/survey
v0.6.1
Published
Self-contained React widget for configurable Lumi surveys.
Readme
Lumi Survey
📖 Ny bruker? Se Kom i gang-guiden for å komme raskt i gang.
Selvstendig React-widget for å samle inn brukertilbakemeldinger via Lumi.
Ny her? Følg Kom i gang i rot-README for installasjon, backend-oppsett og NAIS-tilgang. Denne guiden dekker widget-konfigurasjon og avanserte features.
Public API
Følgende symboler er pakkens stabile, offentlige API. Alt annet er internt og kan endre seg uten varsel — ikke importer fra @lumianalytics/survey/dist/... eller forvent at engine-helpers er tilgjengelige.
Komponent
LumiSurveyDock— selve widgeten. Render én gang per surveyId.LumiSurveyDockProps— props-type.LumiSurveyBehavior,LumiSurveyIntroConfig,LumiSurveyLabels,LumiSurveyStyle,LumiSurveySuccessConfig,StorageStrategy,LumiSurveyConsentResolver,LumiSurveyConsentDecision,StorageAdapter,StorageWriteResult,MaybePromise— sub-typer for dock-konfigurasjon.
Survey-config
LumiSurveyConfig,SurveyType— toppnivå-konfigurasjon.- Spørsmålstyper:
LumiSurveyQuestion(union),LumiSurveyQuestionType,LumiSurveyQuestionBase,EmojiRatingQuestion,ThumbsRatingQuestion,StarRatingQuestion,NpsRatingQuestion,RatingQuestion,TextQuestion,ChoiceQuestion,ChoiceOption,MultiChoiceVariant,RatingVariant,RatingScaleLabel. - Branching:
LogicCondition,LogicOperator,LogicField,LogicAction,LogicActionType,LogicRule. - Verdier:
LumiSurveyAnswerValue.
Transport / submission
LumiSurveyTransport— interfacet du implementerer for å sende submissions til backend.LumiSurveySubmission,LumiSurveyTransportPayload,TransportAnswer,LumiSurveyContext,DeviceType.LumiSurveyEvents,LumiSurveyStatus.- Resultattyper:
LumiSurveySubmitResult,LumiSurveySubmitSuccess,LumiSurveySubmitFailure. - Feiltyper:
LumiSurveyError(union),LumiSurveyValidationError,LumiSurveyTransportError,LumiSurveyDefinitionConflictError. LumiSurveyDefinitionConflictError.diffRedactedmarkerer atdiffkan inneholde redigerte felt- eller option-id-er av personvernhensyn.- Type guard:
isDefinitionConflictError.
Presets
- Builders:
createRatingSurvey,createDiscoverySurvey,createTopTasksSurvey,createTaskPrioritySurvey. - Defaults:
DEFAULT_SURVEY_RATING,DEFAULT_SURVEY_NPS,DEFAULT_SURVEY_DISCOVERY,DEFAULT_SURVEY_SERVICE_FEEDBACK,DEFAULT_SURVEY_STARS,DEFAULT_SURVEY_THUMBS.
Spørsmåls-renderingsprimitiver (for konsumenter som bygger egne dock-UI-er)
MultiChoiceField,SingleChoiceField,useChoiceOptions,RatingQuestionField,getRatingLabel,TextQuestionField,DefaultQuestionRenderer.
Avansert: imperativ hook
useLumiSurvey,UseLumiSurveyOptions,UseLumiSurveyReturn— for konsumenter som vil styre lifecycle selv utenLumiSurveyDock.
API-kontrakter (re-eksportert for konsumenter som integrerer direkte mot backend)
LumiApiAnswer,LumiApiError,LumiApiErrorType,LumiApiFeedbackSubmission,LumiApiFeedbackSubmissionV2,LumiApiSubmissionCreatedResponse.
LumiApiFeedbackSubmission er primæraliaset for V2-kontrakten. LumiApiFeedbackSubmissionV1 eksporteres ikke fra pakkens root fordi backend avviser schemaVersion: 1; bruk LumiApiFeedbackSubmission eller LumiApiFeedbackSubmissionV2 ved direkte integrasjon.
0.6.0 breaking:
LumiApiFeedbackSubmissionV1er fjernet fra root-exporten; brukLumiApiFeedbackSubmissionellerLumiApiFeedbackSubmissionV2. Tidligere (≤0.5.0) lekket ogsåbranchingEngine,evaluateVisibility,computeReachableSteps,buildDefinitionBlock,canonicalForm,computeDefinitionHash,createRatingLabels,DEFAULT_RATING_LABELS,RATING_SCALESog diverse interne types viaexport *-barrels. Disse er nå interne. Hvis du har en reell use case for et fjernet symbol — åpne en issue og foreslå en stabilisert variant.
Sett opp en survey
Her er et komplett eksempel — en emoji-rating med valgfri oppfølgingstekst:
import "@lumianalytics/survey/styles.css";
import { LumiSurveyDock } from "@lumianalytics/survey";
import type { LumiSurveyTransport } from "@lumianalytics/survey";
const transport: LumiSurveyTransport = {
submit: async (submission) => {
await fetch("/api/survey", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(submission.transportPayload),
});
},
};
const survey = {
type: "rating",
questions: [
{
id: "rating",
type: "rating",
variant: "emoji",
prompt: "Hvordan var opplevelsen din?",
required: true,
},
{
id: "feedback",
type: "text",
prompt: "Har du andre tilbakemeldinger?",
maxLength: 1000,
visibleIf: { field: "ANSWER", questionId: "rating", operator: "EXISTS" },
},
],
};
<LumiSurveyDock surveyId="min-flate" survey={survey} transport={transport} />;visibleIf gjør at tekstfeltet først vises etter at brukeren har valgt en emoji. Se Progresjon for flere eksempler.
Spørsmålstyper
En survey er et LumiSurveyConfig-objekt med spørsmål i rekkefølge.
Rating
Fire varianter med ulik skala:
| Variant | Skala | Beskrivelse |
| :--- | :--- | :--- |
| emoji | 1–5 | 😡🙁😐😀😍 |
| thumbs | 1–2 | 👎👍 |
| stars | 1–5 | ⭐⭐⭐⭐⭐ |
| nps | 0–10 | Nummerte knapper med lav/høy-label |
{
id: "nps",
type: "rating",
variant: "nps",
prompt: "Hvor sannsynlig er det at du vil anbefale oss?",
lowLabel: "Lite sannsynlig",
highLabel: "Svært sannsynlig",
required: true,
}Text
Fritekstfelt med valgfri maks-lengde.
{
id: "comment",
type: "text",
prompt: "Hva kan vi forbedre?",
maxLength: 1000,
}Single choice / Multi choice
{
id: "reason",
type: "singleChoice",
prompt: "Hva var du her for å gjøre?",
options: [
{ value: "apply", label: "Søke om noe" },
{ value: "status", label: "Sjekke status" },
{ value: "other", label: "Annet" },
],
}multiChoice støtter variant: "checkbox" (default) eller variant: "combobox" for mange valg.
Progresjon: visibleIf
For de fleste surveyer er det nok å vise oppfølgingsspørsmål kun når det er relevant (progressive disclosure).
{
id: "comment",
type: "text",
prompt: "Hva gikk galt?",
visibleIf: {
field: "ANSWER",
questionId: "rating",
operator: "LT",
value: 3,
},
}Operatorer: EXISTS, EQ, NEQ, GT, LT, CONTAINS.
Storage-strategi
Widgeten husker at brukeren har lukket (dismissed) surveyen, og respekterer en valgfri cooldown-periode før den vises igjen. Du velger hvordan den husker dette:
| Strategi | Merknad |
| :--- | :--- |
| localStorage (default) | Bruker localStorage direkte. Host-appen er ansvarlig for eventuell CMP-/cookie-samtykkevurdering. |
| sessionStorage | Husker dismissal i fanen/sessionen, men ikke etter at nettleserfanen lukkes. |
| none | Ingen persistering. Surveyen vises hver gang, med mindre host/backend dedupliserer selv. |
Når storageStrategy er "none" har ikke widgeten et lokalt signal for at samme
bruker allerede har svart. Hvis backend skal deduplisere slike innsendinger på
tvers av sidelastinger, send en stabil behavior.deduplicationKey. Nøkkelen må
være en opaque ikke-PII token, for eksempel en HMAC fra host-backenden. Ikke send
fødselsnummer, e-post, rå bruker-id eller andre direkte identifikatorer.
Lumi validerer bare lengde og trygg token-syntaks; host-backenden har ansvar for
at verdien ikke er PII. Bruk feltet først når innsending går mot en Lumi API-versjon
som støtter deduplicationKey.
<LumiSurveyDock
behavior={{
storageStrategy: "none",
deduplicationKey: serverIssuedSurveyToken,
}}
// ...
/>Pakken bygger ikke egen samtykke-UI. Dersom host-appen allerede har Cookiebot, OneTrust eller en annen CMP kan den sende inn en consentResolver. Callbacken evalueres lazily før lesing, skriving og sletting av dismissal-state, så endringer i samtykke underveis i sesjonen respekteres:
const consentResolver = useCallback(
() => ({
storage: window.CookieConsent?.hasConsent("functionality") === true,
}),
[],
);
<LumiSurveyDock
behavior={{
consentResolver,
}}
// ...
/>Når consentResolver returnerer false eller { storage: false }, gjør widgeten ingen storage-operasjoner. Det betyr også at gamle dismissal-nøkler ikke ryddes mens samtykke er trukket tilbake; de kan bli liggende til samtykke gis igjen eller host-appen sletter dem selv.
For egen lagringsmekanisme kan host-appen sende inn en storageAdapter. En custom adapter overstyrer storageStrategy, men er fortsatt styrt av consentResolver hvis begge er satt. Hvis write returnerer void, tolkes det som vellykket lagring; returner StorageWriteResult for å rapportere nektet/no-op eller feil:
const storageAdapter = useMemo(
() => ({
read: (key) => myStore.get(key) ?? null,
write: (key, value) => {
myStore.set(key, value);
},
remove: (key) => {
myStore.delete(key);
},
}),
[myStore],
);
<LumiSurveyDock
behavior={{
storageAdapter,
}}
// ...
/>Hold consentResolver og storageAdapter referensielt stabile i React, for eksempel med useCallback og useMemo, slik at widgeten ikke må bygge adapteren på nytt ved hver render.
events.onDismissalPersistFailed fyres når widgeten forsøker å skrive dismissal-state og forsøket feiler, for eksempel ved full storage-kvote, blokkert localStorage/sessionStorage, eller når en custom adapter returnerer { persisted: false, allowed: false, error }, returnerer { persisted: false, allowed: true }, eller kaster. Eventet fyres ikke for bevisste no-op-stier som storageStrategy: "none", samtykke nektet eller SSR.
Widgeten lagrer bare UX-state, ikke svar eller fritekst:
| Key | Verdi | Hvorfor |
| :--- | :--- | :--- |
| lumi-dismissed-${surveyId} | JSON med version, state, dismissedAt, resumeAt og hideCompletely | Hindrer at samme survey vises igjen før cooldown er utløpt, eller etter fullført innsending når hideAfterSubmit er aktiv. |
Pakken eksporterer ferdige surveyer og builder-funksjoner for de vanligste brukscasene:
import {
DEFAULT_SURVEY_RATING,
DEFAULT_SURVEY_DISCOVERY,
DEFAULT_SURVEY_SERVICE_FEEDBACK,
createTopTasksSurvey,
createTaskPrioritySurvey,
createRatingSurvey,
createDiscoverySurvey,
} from "@lumianalytics/survey";| Preset / Builder | Beskrivelse |
| :--- | :--- |
| DEFAULT_SURVEY_RATING | Emoji-rating + valgfri fritekst |
| DEFAULT_SURVEY_DISCOVERY | "Hva kom du hit for å gjøre?" + oppfølging |
| DEFAULT_SURVEY_SERVICE_FEEDBACK | Tjenestevurdering med detaljer |
| createRatingSurvey({...}) | Tilpasset rating med egne spørsmål og oppfølging |
| createDiscoverySurvey({...}) | Tilpasset discovery-survey |
| createTopTasksSurvey({tasks}) | Top Tasks (McGovern-metoden) |
| createTaskPrioritySurvey({tasks}) | Task Priority / Long Neck-rangering |
Eksempel med builder:
const topTasks = createTopTasksSurvey({
tasks: [
{ value: "apply", label: "Søke om sykepenger" },
{ value: "status", label: "Sjekke status" },
],
});
<LumiSurveyDock surveyId="top-tasks" survey={topTasks} transport={transport} />;Rask tommelfingerregel for å velge riktig surveytype. Poenget er å få handlingsbare data med minst mulig friksjon for brukeren.
| Surveytype | Når bruke | Hva du får ut | Typiske fallgruver |
| --- | --- | --- | --- |
| rating | "Pulse" etter en konkret oppgave eller flyt | Trend over tid + (valgfri) årsak i fritekst | For generelt spørsmål, for mange spørsmål, for hyppig visning |
| discovery | Utforskning: hva kom brukeren hit for å gjøre? | Frie tekstsvar + "fikk du gjort det?" + ev. blocker | For mye tekst, dårlig segmentering, samler identifikatorer i context |
| topTasks | Måle suksess for kjerneoppgaver (McGovern) | Suksess/feil per oppgave + blocker-innsikt | For mange/få oppgaver, ikke randomisert rekkefølge, uklare oppgavenavn |
| taskPriority | Strategisk: hva er viktigst å prioritere? (Long Neck) | Rangering av viktigste oppgaver (top N) | For få tasks, ikke randomisert, feil UI-variant for mange tasks |
| custom | Når du må kombinere eller branch'e | Skreddersydd spørreflyt | Blir fort "for mye", vanskelig å sammenligne over tid |
Anbefaling: Start med rating eller discovery, og gå videre til topTasks/taskPriority når dere har en tydelig hypoteseliste.
Best practices
- Hold det kort: 1–2 spørsmål er ofte nok (rating + valgfri tekst).
- Still spørsmål om en konkret opplevelse ("etter du gjorde X"), ikke hele produktet.
- Bruk progresjon: vis fritekst først etter at rating er valgt (
visibleIf). - Bruk
context.tagsfor segmentering (lav kardinalitet), ogcontext.debugkun for feilsøking (høy kardinalitet). - Unngå identifikatorer i
context(og ikke auto-collectpathnamepå dynamiske ruter). - Velg en stabil
surveyIdper flate/bruksmønster (ikke per deploy).
Go-live sjekkliste
- Importer styling:
@lumianalytics/survey/styles.css. - Implementer
transport.submitsom sendersubmission.transportPayloadtil din backend. - Sett riktig
storageStrategy(localStorage/sessionStorage/none) eller brukconsentResolver/storageAdapter. - Test ende-til-ende (innsending → dashboard).
LumiSurveyDock props
| Prop | Type | Påkrevd | Beskrivelse |
| :--- | :--- | :---: | :--- |
| surveyId | string | ✅ | Unik identifikator for surveyen (f.eks. soknad-kvittering) |
| survey | LumiSurveyConfig | ✅ | Konfigurasjonsobjektet for spørsmålene |
| transport | LumiSurveyTransport | ✅ | Objekt med submit-funksjon for innsending |
| context | object | ❌ | Metadata/tags/debug for segmentering |
| behavior | object | ❌ | Styrer åpning, lukking, cooldown og storage-strategi |
| events | object | ❌ | Event-callbacks for sporing og livssyklus |
| labels | object | ❌ | Tekster for UI-elementer (send-knapp, feilmeldinger) |
| success | object | ❌ | Konfigurer suksess-visning (tittel, tekst, auto-lukk) |
| style | object | ❌ | Visuell styling (posisjon, farger, classNames) |
Transport og payload
Widgeten sender submission.transportPayload til din backend. Payloaden er stabil og
versjonert (schemaVersion: 2). Den inkluderer:
surveyId,surveyType,submittedAt,startedAtdeduplicationKeynår host-appen eksplisitt sender en opaque nøkkelanswers: Normalisert struktur per spørsmåldefinition: Surveyens feltstruktur slik backend validerer og registrerer immutable definitioncontext: tags/debug/auto-collectet miljøinfo
Backend-oppsett (token exchange, NAIS-tilgang) er beskrevet i Koble til backend.
Kontekst og personvern
Auto-collectes i browser:
viewport(bredde/høyde)deviceType(mobile/tablet/desktop)userAgent
url og pathname auto-collectes ikke som default. Hvis dere har statiske ruter uten
identifikatorer kan dere opt-in:
<LumiSurveyDock behavior={{ collectLocation: true }} />Hvis rutene kan inneholde ID-er, send heller en sanitert verdi:
<LumiSurveyDock context={{ pathname: "/sak/:id" }} />Tags vs debug
context.tags: Lav kardinalitet, brukes til segmentering og grafer i dashboard.context.debug: Høy kardinalitet, kun for detaljvisning av enkeltinnsendinger.
Events (hooks)
Registrer event-callbacks ved å sende et events-objekt til LumiSurveyDock.
const events = {
onViewDock: (surveyId) => {},
onAnswer: (questionId, value) => {},
onSubmitStart: (submission) => {},
onSubmitSuccess: (submission) => {},
onSubmitError: (cause) => {},
onValidationFailed: (missingQuestionIds) => {},
onReset: () => {},
onDismissalPersistFailed: (cause) => {},
};Bruk logic når du faktisk må endre flyten (hoppe, skippe, eller avslutte tidlig). Hvis du bare vil vise/skjule oppfølgingsspørsmål, bruk heller visibleIf.
const surveyWithLogic = {
type: "custom",
questions: [
{
id: "rating",
type: "rating",
prompt: "Hvor fornøyd er du?",
logic: [
{
condition: { field: "ANSWER", operator: "LT", value: 3 },
action: { type: "JUMP_TO", targetId: "comment" },
},
{
condition: { field: "ANSWER", operator: "GT", value: 2 },
action: { type: "SUBMIT" },
},
],
},
{
id: "comment",
type: "text",
prompt: "Hva kan vi forbedre?",
},
],
};Feilsøking (vanlige problemer)
- Survey dukker ikke opp: Sjekk at
behavior.initialOpenikke er satt tilfalse, og atstorageStrategyikke skjuler den pga. cooldown. - Ingen data i dashboard: Verifiser at backend sender
submission.transportPayloadtil riktig endepunkt. - Layout virker "tom": Sørg for at
@lumianalytics/survey/styles.csser importert. - Dismissed-tilstand persisteres ikke: Sjekk at
storageStrategyikke er satt til"none".
