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

@intunix/core

v1.2.1

Published

Renderer-agnostic core of the official Intunix SDK — the AI-native experience management platform for in-app surveys, NPS, CSAT and feedback forms.

Readme

@intunix/core

Renderer-agnostic core of the official SDK for Intunix, the AI-native experience management platform — the logic layer behind in-app surveys, NPS, CSAT, and feedback forms.

It contains the IntunixClient (event queue, trigger evaluator, experience manager, offline persistence, session tracking) and the framework-agnostic FeedbackRunner + types. You normally consume it through a binding rather than directly:

Install

npm install @intunix/core

Direct use

import { IntunixClient } from '@intunix/core';

const client = new IntunixClient({ renderer: myRenderer });

// Register makes a form auto-show when its trigger matches. Do it at init…
await client.init('pk_live_xxx', { register: ['nps_q4'] });
// …or anytime later (returns an unregister fn):
const off = await client.register('exit_survey');

client.identify('u_42', { plan: 'pro' });
client.track('checkout_completed', { value: 99 });

client.showForm('nps_q4');   // or just open one manually, no register needed

client.reset();              // on logout — see "Identity, and what reset() is for"

A Renderer decides how experiences are displayed — see the React/Vue/web bindings for reference implementations.

How forms show

  • Opt-in: a form auto-triggers only after you register it (by slug or id). Unregistered forms never pop on their own — but showForm(slug) still opens any form manually. register also controls what gets fetched at all, and is only half the story — see Register modes and the console gate below.
  • One at a time: trigger matches and manual showForm calls funnel through one queue, so forms appear in sequence, never stacked.
  • URL delay: URL-triggered forms wait urlTriggerDelayMs (default 600) so they don't pop the instant a page loads.
await client.init('pk_live_xxx', { register: ['welcome', 'nps_q4'], urlTriggerDelayMs: 600 });

Register modes and the console gate

register is typed string[] | 'all', and it drives the fetch itself, not just which triggers go live:

| Config | Fetched | Behavior | |---|---|---| | register: 'all' | every SDK-enabled form | autoActivate forms self-show | | register: ['nps_course', 'csat'] | only those | their triggers go live | | omitted / [] | nothing — no catalog request is made at all | only showForm(id) pulls that one form, on demand, via the single-form endpoint |

A form is delivered to the SDK only when both independent gates agree:

  1. sdk_enabled — "Available in Web SDK" is toggled on for the form in the Intunix console.
  2. register — the host's register list includes the form's slug/id, or register is 'all'.

The backend enforces gate 1 unconditionally — a form the console hasn't enabled for the Web SDK is excluded from the response regardless of register. Gate 2 is enforced by what the host actually asks for. This pairing is the #1 source of "I registered my form and nothing loaded" — verify both sides, not just the register call, before assuming a bug.

Implementation details that follow from this:

  • The requested form set is sorted before being sent (ExperienceManager.formsParam), so register: ['b', 'a'] and register: ['a', 'b'] produce the identical ?forms=a,b request and share a cache entry.
  • The catalog cache (ExperienceCache) carries two fingerprints: the session it was fetched for, and the requested form set. A cache hit requires both to match the live session and the live register — a changed register list invalidates the cache immediately, even mid-session, independent of the session-start fetch policy below.
  • The backend's ETag is W/"<version>-<hash of requested set>", so two hosts (or the same host across a config change) requesting different register lists never share a 304 with each other.

Context

Attach dynamic context — course id, screen, plan tier, anything — to targeting, responses, and cooldown scoping, without a redeploy.

  • Sticky context merges into a session-scoped bag and stays attached to every show/response until cleared or a new session starts:

    client.setContext({ courseId: 'c-101' });
    client.setContext(null);   // or client.clearContext()
  • Per-call context is scoped to a single showForm/showExperience and wins over sticky context for that call:

    client.showForm('nps_course', { context: { courseId: 'c-102' } });

Context flows into every response payload as a top-level context object (so the console can segment feedback per course/screen), into event batch entries, and into targeting.rules entries with kind: 'context'. Use frequencyScope: 'form_context' (below) to keep cooldowns independent per context value.

Identity, and what reset() is for

The SDK needs an identity to key cadence records against — "shown once per user", "quiet for 30 days after they answer". Most visitors are anonymous, so it mints a random anonymous id on first visit (localStorage, intunix:anon) and uses it whenever no user is identified:

const uid = user?.id ?? anonId();

identify(userId) moves everything recorded under the anonymous id onto the real one. So the anonymous id only ever holds activity since the last identify() — a form dismissed while logged out still counts after logging in.

client.identify('u_42');   // anon records move to u_42; the anon key is now empty
client.reset();            // on logout

What reset() actually changes

Call it on logout. It forgets the identified user, rotates the anonymous id, ends the session, and clears sticky context.

It is not what keeps one user's cooldowns off another user. identify() already does that, because it drains the anonymous key on every login — a returning user's caps live under their own id, not the shared anonymous one. If you call identify() on every login, gating stays correct even without reset().

What you lose by skipping it, on a shared browser:

| Without reset() | Consequence | |---|---| | Session is not ended | The next person lands inside the previous session (30-min TTL), so every_session will not re-fire for them. | | Sticky context survives | intunix:context is sessionStorage-scoped, so the previous user's context attaches to the next person's responses until the tab closes. | | Anonymous id is not rotated | Both people's logged-out responses carry the same anonymous_id, so reporting counts them as one respondent — and it stays that way in the database, permanently. |

That last one is the reason to bother: the others self-heal, but a response is written once and keeps whatever ids it had at submit time.

If your logout is a single-page transition with no page reload, reset() matters more: the identified user is held in memory, so without it the SDK keeps attributing responses to the previous user until the next identify() lands.

What it deliberately keeps

The form catalog, and the per-device firstSeenAt / sessionCount that eligibility.minSessions and minDaysSinceFirstSeen gate on. Those are device-level, not user-level — clearing them would let anyone reset themselves to "brand-new user" by logging out.

Console-driven catalog fields

Everything below is set per-form in the Intunix console — no SDK update or site redeploy required. Fields are optional; a form or an older catalog without them behaves exactly as it does today.

| Field | Meaning | Example | |---|---|---| | autoActivate | Form auto-triggers on catalog load — no register() call needed in host code. | autoActivate: true | | targeting.traitEquals | Legacy: match identified-user traits by equality (ANDed with rules). | { plan: 'pro' } | | targeting.rules | { kind: 'trait' \| 'context' \| 'url', key?, op, value? }[], ANDed. op is one of eq \| neq \| in \| nin \| gt \| lt \| contains \| exists. | { kind: 'context', key: 'courseId', op: 'eq', value: 'c-101' } | | eligibility.minDaysSinceFirstSeen | Skip users first seen fewer than N days ago. | 3 | | eligibility.minSessions | Skip users with fewer than N sessions. | 2 | | eligibility.startAt / endAt | ISO date-time scheduling window the form is eligible within. | "2026-08-01T00:00:00Z" | | cadence.mode | How often the form may show: once | every_session | cooldown | always. | 'cooldown' | | cadence.cooldown.answered | After the user answers, wait this long before showing again. Days, 'session', or 'never'. | 30 | | cadence.cooldown.dismissed | After the user closes it without answering, wait this long. | 7 | | cadence.cooldown.ignored | After the user ignores it (no interaction), wait this long. | 1 | | cadence.giveUpAfterShows | Stop forever after this many shows with no answer. Ignored when mode: 'always'. | 3 | | frequencyScope | Cooldown key: 'form' (default — one cooldown per user) or 'form_context' (cooldown scoped per context value named by the form's kind: 'context' targeting rules). | 'form_context' | | capabilities.batchShown | Catalog-level (not per-form): impressions/dismissals ride in the /sdk/events batch as $form_shown/$form_dismissed instead of a per-impression POST. Controlled server-side. | true |

Evaluation order at show time: targetingeligibilitycadence. All must pass.

Every cadence wait measures from the last time the form was shown; the outcome only selects which wait applies. every_session is shorthand for a 'session' wait on every outcome, and once for a 'never' wait — so there is one rule to reason about, not two that interact.

Request budget

What actually goes over the wire — the part third-party integrators care about:

  • The catalog (GET /sdk/experiences?forms=…) is fetched once per session, for the currently registered form set, at session start. A page load, reload, or SPA navigation within a live session, with the same register, makes zero catalog requests — the localStorage cache is authoritative and is stamped with both the session and the form set it was fetched for.
  • Changing register (including switching to/from 'all') invalidates the cache immediately, even mid-session, and triggers an immediate refetch.
  • Passing no register (or []) skips the catalog fetch entirely — the experiences list stays empty and showForm(id) resolves that one form on demand via GET /sdk/forms/:id instead.
  • The first fetch for a given session+set sends If-None-Match and is normally a 304 Not Modified (no body).
  • There is no POST /sdk/init — SDK name/version rides on an X-Intunix-Sdk header on the catalog GET instead.
  • Impressions and dismissals ride inside the periodic /sdk/events batch as $form_shown / $form_dismissed (when the catalog's capabilities.batchShown is on), not as a per-impression POST.
  • Events flush on a timer (flushIntervalMs, default 5000 ms) and via navigator.sendBeacon on pagehide, so a pending batch isn't lost on tab close.

License

Proprietary © Intunix.