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

@bakaburg24/decisioning-js

v0.1.5

Published

Decisioning platform SDK for browsers. Calls /v1/decide, manages anonymous_id, fires telemetry, renders via a tenant-supplied adapter. Zero runtime deps.

Readme

@bakaburg24/decisioning-js

Browser SDK for the decisioning platform. Drop it on your site once — the platform decides what to show, when, and to whom.

pnpm add @bakaburg24/decisioning-js

Quick start — autonomous mode (recommended)

You declare slots once. The SDK observes user behavior (page-views, scroll, dwell, idle, exit-intent), calls /v1/decide per slot, installs the variant's trigger, and renders only when the platform-supplied trigger fires.

import { DecisioningClient } from '@bakaburg24/decisioning-js';

const client = new DecisioningClient({
  apiKey: process.env.NEXT_PUBLIC_DECISIONING_KEY!,
  endpoint: 'https://decision-service-production.up.railway.app',
  eventsEndpoint: 'https://event-ingest-production-5912.up.railway.app',
  consent: { behavioral: true, cookies: true },
  renderAdapter: async (variant, ctx) => {
    const el = document.createElement('div');
    el.innerHTML = `
      <h3>${variant.creative.headline}</h3>
      <p>${variant.creative.subhead}</p>
      <button>${variant.creative.cta}</button>
    `;
    el.querySelector('button')!.addEventListener('click', () => {
      ctx.sdk.trackConversion({ decisionId: ctx.decisionId, outcomeName: 'newsletter_signup' });
      ctx.sdk.unmount(ctx.decisionId);
    });
    ctx.target.appendChild(el);
    return { unmount: () => el.remove() };
  },
});

// Once per page. SDK does everything else.
client.observe([
  { name: 'newsletter', target: () => document.getElementById('newsletter-slot') },
  { name: 'cross_promo', target: () => document.getElementById('cross-promo-slot') },
]);

That's it. The operator hardcodes nothing about when to show — variants carry their trigger config, set by the AI generator or by the operator in the wizard. The platform's bandit (Phase 2+) learns which trigger+variant combo wins per audience.

What observe() does behind the scenes

| Step | Detail | |---|---| | 1. Telemetry start | Fires page-view event (URL, referrer, locale, device class, viewport). Arms scroll-depth milestones (25/50/75/100%) and a 30s dwell pulse. | | 2. Decisions | Calls /v1/decide once per slot with the SDK-managed anonymous_id + your context. | | 3. Trigger install | Reads the variant's trigger (immediate / scroll / dwell / idle / exit-intent / custom-event) and installs the matching listener. | | 4. Render | When the trigger fires, the SDK looks up the slot's target, calls the render adapter, fires the impression event server-side (via decision-service). | | 5. Cleanup | stopObserving() tears down all listeners. Call it on SPA route change before the next observe(). |

Custom trigger from your code

If a variant uses custom-event type, dispatch the matching event from your own code at the right moment:

// Variant has trigger { type: 'custom-event', name: 'puzzle:completed' }
document.dispatchEvent(new CustomEvent('puzzle:completed'));

This lets you bridge app-specific moments into the platform's trigger model without hardcoding "when" elsewhere.

Manual mode — single decision

When you want to control /decide explicitly (e.g. server-rendered slots, deferred mount):

await client.decideAndRender({
  slot: 'newsletter',
  context: { page: { topic: 'finance' } },
  target: document.getElementById('newsletter-slot')!,
});

Returns the DecideResponse so you can inspect or skip rendering yourself.

Anonymous ID + consent

  • Anonymous ID is generated on first call, stored in localStorage (1-year sliding expiry), falls back to cookie, then to per-page UUID.
  • Consent gating — when consent.behavioral is false, the SDK omits the persisted anonymous_id (sends "anonymous") so decisions are context-only.
  • Update at runtimeclient.setConsent({ behavioral: true, cookies: true }). Existing cached decisions are not invalidated.

Telemetry events fired automatically

| Event | When | Payload | |---|---|---| | engagement:page_view | Every observe() call | URL, referrer, locale, device class, viewport | | engagement:scroll_depth | Crossing 25 / 50 / 75 / 100% | depth_pct | | engagement:dwell_pulse | Every 30s while tab is foreground | interval_ms | | impression | When a variant renders | Server-emitted by decision-service |

Manual events (call from your code):

client.trackEngagement({ decisionId, engagementType: 'click', payload: { target: 'cta' } });
client.trackConversion({ decisionId, outcomeName: 'newsletter_signup', value: 1.0 });
client.trackDismiss({ decisionId, dismissedAfterMs: 4200 });

Render adapter contract

type RenderAdapter = (
  variant: Variant,
  ctx: { decisionId: string; target: HTMLElement; sdk: DecisioningClientApi },
) => Promise<{ unmount: () => void } | void> | { unmount: () => void } | void;

Return { unmount } so client.unmount(decisionId) can clean up on route change.

What the SDK doesn't do

  • Bundle any framework. Render adapters are the framework boundary.
  • Retry failed /v1/decide (1000ms timeout by default, then cache fallback or no-op — retries hurt conversion more than they help). Override with timeoutMs.
  • Block page load. Initialize early; lazy slot targets are fine.

What a variant looks like (generic creative shape)

A variant returned from /v1/decide carries everything needed to render an end-to-end UX, not just copy:

{
  variant_id: "v_xxx",
  slot: "newsletter",
  format: "modal" | "slide-in" | "inline" | ...,
  placement: "...",
  tags: [...],
  creative: {
    headline: string,
    subhead: string,
    cta: string,
    kind?: "newsletter" | "discount" | "upsell" | "lead_capture" | "survey" | "announcement",
    fields?: Array<{ type: "email" | "text" | "textarea" | "tel", name, label?, placeholder?, required? }>,
    submit?:
      | { kind: "mailflix" }
      | { kind: "external_link", url: string }
      | { kind: "webhook", url: string }
      | { kind: "none" },
    success_message?: string,
    trigger?: { type, ... },
  }
}

Render adapters can read creative.fields + creative.submit and build the form dynamically. See the host-app example in examples/ for a reference adapter that handles all four submit kinds.

The full schema + examples live in decisions/docs/VARIANT_CREATIVE_SHAPE.md.

Changelog

0.1.5 — Forward all form values downstream

The bundled default renderer's Mailflix POST previously dropped every form field except email and name. For survey or lead-capture variants with a textarea, the actual response content (typed by the user) was sent to the platform's conversion event payload but never reached Mailflix's metadata — so the operator could see "someone converted" but not what they actually said.

Now: every non-email/name form value gets nested into Mailflix's metadata blob alongside decision_id + variant_id, so survey responses are recoverable from Mailflix's subscriber UI. (Same data also lands in the platform's events warehouse via trackConversion's properties field — that's now visible on the portal's decision drawer via a new "Response" card.)

0.1.4 — One-file embed

The SDK now ships a bundled vanilla-DOM renderer that handles every variant format (modal, slide-in, inline, bottombar, topbar) and submit kind (mailflix, external_link, webhook, none) without operator code. renderAdapter is now optional on the client constructor; observe() can be called with no arguments and auto-discovers slots from the DOM (elements with data-decisioning-slot="<name>"), falling back to a single floating slot when none exist.

Minimal embed becomes:

import { DecisioningClient } from '@bakaburg24/decisioning-js';

const client = new DecisioningClient({
  apiKey: '...',
  endpoint: 'https://decision-service-production.up.railway.app',
  eventsEndpoint: 'https://event-ingest-production-5912.up.railway.app',
  consent: { behavioral: true, cookies: true },
});
client.observe(); // no slots arg, no renderAdapter — just works

Brand tokens auto-extract from the host page's CSS. The SDK reads --decisioning-primary (or --primary), --decisioning-surface, --decisioning-text, --decisioning-radius, --decisioning-font from :root, falls back to the host body's computed font + colors, then to neutral defaults (indigo accent, system font, white surface). Pass explicit brand: { primary, surface, text, radius, font } at client init to override.

Conversion tracking decoupled from downstream POST: the bundled renderer fires trackConversion the moment form submit / CTA click fires — before hitting Mailflix or the operator's webhook. A misconfigured downstream key or transient 5xx no longer silently blinds the bandit. (Custom render adapters should follow the same pattern.)

0.1.3

  • Debug logging gate. Set localStorage.dx_debug = '1' in the browser console and reload — the SDK then logs every lifecycle moment to the console with the [decisioning] prefix: observe() called, /v1/decide resolved (and what variant + trigger was picked), trigger armed, trigger fired, render adapter invoked, adapter returned. Useful for diagnosing "why isn't my variant rendering" on production sites without redeploying. Off by default — negligible runtime cost when the flag is unset (one localStorage check at SDK init, cached).

0.1.2

  • Exit-intent trigger fix. The previous implementation listened for mouseleave on document, which is unreliable across browsers (mouseleave doesn't bubble and document's leave-boundary is browser-dependent — variants configured with trigger.type: 'exit-intent' never actually fired in production). Now uses mouseout on document with relatedTarget=null + clientY<=0, the browser-portable signal for "cursor crossed the top edge."

0.1.1

  • DEFAULT_TIMEOUT_MS raised from 200 → 1000. The old default matched decision-service's server-side latency target but didn't account for browser→Railway network cost (TLS + RTT is 100–300ms from EU). Server responses that beat the old timeout still fired impressions (decision-service emits them async before responding), but the SDK aborted the body read and the variant copy never swapped in. Override via timeoutMs for same-region setups where 200ms is realistic.

0.1.0

  • Initial release. DecisioningClient, observe(), render-adapter contract, anonymous-id management, telemetry events.