npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.diffRedacted markerer at diff kan 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 uten LumiSurveyDock.

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: LumiApiFeedbackSubmissionV1 er fjernet fra root-exporten; bruk LumiApiFeedbackSubmission eller LumiApiFeedbackSubmissionV2. Tidligere (≤0.5.0) lekket også branchingEngine, evaluateVisibility, computeReachableSteps, buildDefinitionBlock, canonicalForm, computeDefinitionHash, createRatingLabels, DEFAULT_RATING_LABELS, RATING_SCALES og diverse interne types via export *-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.tags for segmentering (lav kardinalitet), og context.debug kun for feilsøking (høy kardinalitet).
  • Unngå identifikatorer i context (og ikke auto-collect pathname på dynamiske ruter).
  • Velg en stabil surveyId per flate/bruksmønster (ikke per deploy).

Go-live sjekkliste

  • Importer styling: @lumianalytics/survey/styles.css.
  • Implementer transport.submit som sender submission.transportPayload til din backend.
  • Sett riktig storageStrategy (localStorage / sessionStorage / none) eller bruk consentResolver/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, startedAt
  • deduplicationKey når host-appen eksplisitt sender en opaque nøkkel
  • answers: Normalisert struktur per spørsmål
  • definition: Surveyens feltstruktur slik backend validerer og registrerer immutable definition
  • context: 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.initialOpen ikke er satt til false, og at storageStrategy ikke skjuler den pga. cooldown.
  • Ingen data i dashboard: Verifiser at backend sender submission.transportPayload til riktig endepunkt.
  • Layout virker "tom": Sørg for at @lumianalytics/survey/styles.css er importert.
  • Dismissed-tilstand persisteres ikke: Sjekk at storageStrategy ikke er satt til "none".