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

@tabcommerceio/cross-sell-sdk

v0.1.5

Published

Storefront JavaScript SDK for Tab Cross Sell: recommendation resolve, valid-impression observation, click / add-to-cart tracking, and batched event delivery.

Readme

Cross Sell SDK

Storefront JavaScript SDK for Cross Sell recommendations. Owns the Cross Sell network layer: recommendation resolve, impression observation, click / add-to-cart tracking, idempotent event_id + occurred_at, and batched event flush.

Theme App Blocks and Agency UIs render their own DOM. The SDK mounts against data-tab-cross-sell roots via host-provided render callbacks.

Product Bundles

Product Bundle Offers use the independent @tabcommerceio/buy-together-sdk. It supplies resolve, exact Variant quote, and verified addAll with Bundle tracking identities. This package continues to handle Cross Sell recommendations; it does not mount Bundle roots or process Bundle events. Existing Cross Sell integrations do not need to migrate because the app adds Product Bundles.

Install both packages when a custom storefront uses both workflows. Alias their init imports and use separate clients, cards, observers, and tokens. The Bundle package README provides the complete quote/add/error-handling example.

Install

npm install @tabcommerceio/cross-sell-sdk
# pnpm add @tabcommerceio/cross-sell-sdk
import { init } from '@tabcommerceio/cross-sell-sdk'

ESM only, no runtime dependencies, ships its own TypeScript declarations. Browser-targeted (ES2022 + DOM APIs) — it is not meant to run on a server.

Inside this monorepo it stays a workspace package ("@tabcommerceio/cross-sell-sdk": "workspace:*"); releases run from the repo root via npm run publish (see scripts/release/publish-sdk.mjs).

The committed manifest version is the release signal. A version change pushed to main is validated and published by GitHub Actions through npm Trusted Publishing in the shared publish-cross-sell-sdk.yml workflow, then tagged as cross-sell-sdk@<version>. That workflow covers both public SDKs independently. Package source metadata points to the private tabcommerce/tab-cross-sell repository. The local command remains an authenticated recovery path.

Theme extension assets are built from src/theme/storefront.ts into apps/theme-extension/assets/tab-cross-sell-storefront.js. That IIFE bundle is internal to the official Theme App Extension and is not part of the npm package.

Public API

const client = init({
  proxyBase: '/apps/tab-cross-sell-dev', // App Proxy prefix/subpath
})

const result = await client.resolve({
  placement: 'pdp',
  source_products: [{ product_id: 'gid://shopify/Product/1' }],
  limit: 3,
})

// Campaign/Rule identity lives on each candidate (ownership edge), not on the
// response root. Cart unions may mix owners across result.candidates /
// result.decisions[].
const candidate = result.candidates[0]!

client.track({
  event_type: 'click',
  tracking_token: candidate.tracking_token,
  candidate_product_id: candidate.product_id,
  request_id: result.request_id,
  campaign_id: candidate.campaign_id,
  rule_id: candidate.rule_id,
  placement: 'pdp',
  attrs: { variant_id: 'gid://shopify/ProductVariant/20' },
})

await client.mount({
  root: document.querySelector('[data-tab-cross-sell="pdp"]')!,
  request: { placement: 'pdp', limit: 3 },
  render(result, root) {
    // Host paints cards; return elements to observe for impressions.
    // Prefer server-rendered card data-* identity over inventing owners.
    root.innerHTML = '...'
    return {
      targets: [...root.querySelectorAll('[data-tracking-token]')].map((el) => ({
        element: el,
        tracking_token: el.getAttribute('data-tracking-token')!,
        candidate_product_id: el.getAttribute('data-product-id')!,
        candidate_variant_id: el.getAttribute('data-variant-id')!,
        request_id: result.request_id,
        campaign_id: el.getAttribute('data-campaign-id')!,
        rule_id: el.getAttribute('data-rule-id')!,
        placement: 'pdp',
      })),
    }
  },
})

await client.flush()
client.destroy()

Resolve response shape

| Field | Meaning | |---|---| | decisions[] | Per-Rule strategy, published versions, fingerprint, Holdout group, and audit attrs. PDP has at most one entry; cart_page / cart_drawer list every matched Rule. | | candidates[] | Globally limited union for cart placements (or the single PDP winner pool). Each candidate carries its ownership-edge campaign_id / rule_id / tracking_token. | | request-level ids | request_id, request tracking_token, placement, composite decision_version, fallback |

Do not read a singular top-level campaign_id / rule_id / strategy / experiment_group from the resolve root — those fields were removed because they cannot honestly describe a multi-Rule cart union. Observer, click, and ATC paths validate per-card data-campaign-id / data-rule-id.

Invariants

| Behavior | Rule | |---|---| | Never block commerce | resolve timeout / 5xx / network error → null; no throw | | Valid impression | ≥50% visible and ≥500ms while the page is visible; one impression per tracking_token | | Event idempotency | retries resend the same event_id + occurred_at | | Event delivery | batches never exceed the API limit of 100 and use fetch keepalive for navigation/page hide | | Flush triggers | interval, queue threshold, visibilitychange, pagehide; failures remain queued for the next trigger | | Variant attribution | impression/click/ATC events include the resolved Variant GID in attrs.variant_id | | Owner-correct events | impression/click/ATC and cart line properties stamp the selected card's Campaign/Rule ownership edge | | Add-to-cart truth | the official Quick Add writes _tab_tracking_token + _tab_request_id as line properties and emits add_to_cart only after Shopify returns success | | Empty-slot stability | PDP starts presence + rendered concurrently and only explicit positive presence reveals a skeleton; Cart Page/Drawer call rendered only; rendered HTML is authoritative everywhere | | Post-Quick-Add stability | preserve the current official markup for one app-originated refresh; later shopper cart edits resolve fresh |

Renderable decision fallback is owned by customer-api so fallback responses can receive fresh request and candidate tracking tokens (reminted from each candidate's own provenance). The SDK returns null on network failure instead of replaying stale renderable decisions.

App Proxy paths

  • POST {proxyBase}/recommendations/resolve
  • POST {proxyBase}/events

proxyBase is /<app_proxy.prefix>/<app_proxy.subpath> from shopify.app.toml.

Agency usage

  1. Install the SDK from npm: npm install @tabcommerceio/cross-sell-sdk.
  2. Call init({ proxyBase }) with the shop’s App Proxy base (or your Agency gateway that preserves the OpenAPI shapes).
  3. Render your own cards; call track / mount as needed.
  4. Include the resolved Variant GID on observe targets/events and stamp the two private attribution properties when your UI adds a recommended line to cart.
  5. Do not depend on official extension class names — only on the data you pass into render / track.

Cart Drawer

Prefer an explicit mount point:

<div data-tab-cross-sell="cart_drawer">...</div>

Then wire the drawer controller so reopen/refresh normally re-resolves. The official integration has one narrow exception: after its own successful Quick Add it may restore the exact current markup through the originating theme redraw without a second rendered request. Unrelated body mutations do not consume the token; a bounded two-second grace covers delayed theme redraws, after which the normal fresh resolve path resumes.

The controller compares native cart-line identity/quantity markers inside the compatible Drawer shell. Shipping-progress, countdown, toast, and other presentation-only mutations do not trigger a recommendation request.

import { createCartDrawerController, init } from '@tabcommerceio/cross-sell-sdk'

const client = init({ proxyBase: '/apps/tab-cross-sell-dev' })
const drawer = createCartDrawerController({
  documentRef: document,
  async mount(root) {
    return client.mount({
      root,
      request: { placement: 'cart_drawer', limit: 3 },
      render(result, mountRoot) {
        // Agency paints cards; return observe targets.
        return { targets: [] }
      },
    })
  },
})
drawer.start()

When no mount point exists, the controller probes a compatible-theme selector whitelist and waits silently on miss so a lazy compatible Drawer can appear later (no DOM writes, recommendation requests, or console errors before a hit). See packages/contracts/schemas/storefront/cross-sell-mount-points.md.