npm package: @scriptive-au/shopify-ss-beacon
Description: SearchSpring Beacon 2.0 tracking for Shopify themes — no build system required
Author: Scriptive
Latest version: 1.1.0

# @scriptive-au/shopify-ss-beacon

SearchSpring Beacon 2.0 tracking for Shopify themes. Works with or without a build system.

## Install

### CDN (no build system — Shopify themes)

```html
<script src="https://cdn.jsdelivr.net/npm/@scriptive-au/shopify-ss-beacon@1/dist/beacon.min.js" defer></script>
```

### npm (stores with a build system)

```bash
npm install @scriptive-au/shopify-ss-beacon
```

```js
import '@scriptive-au/shopify-ss-beacon';
```

## Setup

Set `window.SSBeaconConfig` **before** the script loads. In Shopify, do this in a Liquid snippet rendered at the top of `<body>`:

```liquid
<script>
  window.SSBeaconConfig = {
    siteId: 'YOUR_SITE_ID',
    initiator: 'your-store/custom/1.0.0',
    shopperId: {{ customer.id | json }},
    dev: false,
    cart: {{ cart | json }}
  };
</script>
<script src="https://cdn.jsdelivr.net/npm/@scriptive-au/shopify-ss-beacon@1/dist/beacon.min.js" defer></script>
```

See [example/searchspring-beacon.liquid](example/searchspring-beacon.liquid) for a complete drop-in snippet.

### Config options

| Key | Type | Required | Description |
|-----|------|----------|-------------|
| `siteId` | string | **yes** | SearchSpring site ID |
| `initiator` | string | no | Integration identifier (default: `custom/1.0.0`) |
| `shopperId` | string | no | Logged-in customer ID or email — sent with every event when set |
| `dev` | boolean | no | Sandbox mode — events are tagged as test data (default: `false`) |
| `cart` | object | no | Current Shopify cart object (`{{ cart \| json }}`). Seeds cart diff tracking |

## Usage

The `observe*Grid` methods wire up **impression** and **click** tracking on a grid container. Impressions use `IntersectionObserver` (70 % visible, 1 s dwell), clicks use a delegated handler. Cards added later (infinite scroll, etc.) are picked up automatically.

Each `observe*Grid` call returns a **disconnect function**. Call it before re-observing (e.g. after pagination or filtering) to tear down old observers and listeners:

```js
var disconnect = SSBeacon.observeSearchGrid(responseId, gridEl);

// Later, when results change:
disconnect();
disconnect = SSBeacon.observeSearchGrid(newResponseId, gridEl);
```

**Requirements on your product cards** (for `observe*Grid` only):
- Each card needs a `data-ss-uid` attribute (the product uid from SS)
- Optionally `data-ss-parent-id` (defaults to uid if omitted)

```html
<div class="product-item" data-ss-uid="abc123" data-ss-parent-id="abc123">
  <a href="/products/my-product">...</a>
</div>
```

If `observe*Grid` doesn't suit your setup (custom carousels, virtual lists, etc.), use the manual `*Impression` / `*Click` methods instead. Same item shape, fires immediately:

```js
// Manual impression (e.g. from your own visibility callback):
SSBeacon.searchImpression(responseId, [
  { type: 'product', parentId: '123', uid: '456' }
]);

// Manual click:
SSBeacon.searchClick(responseId, { type: 'product', parentId: '123', uid: '456' });
```

### Search

[Tracking guide](https://docs.searchspring.com/reference/search-tracking-guide)

```js
// After rendering search results:
var disconnect = SSBeacon.observeSearchGrid(responseId, gridElement);
SSBeacon.searchRender(responseId);

// If SS API returns a redirect instead of results:
SSBeacon.searchRedirect(responseId, redirectUrl);

// Quick add to cart from search grid:
SSBeacon.searchAddToCart(responseId, [
  { parentId: '123', uid: '456', qty: 1, price: 29.99 }
]);
```

### Category

[Tracking guide](https://docs.searchspring.com/reference/category-tracking-guide)

```js
SSBeacon.categoryRender(responseId);
var disconnect = SSBeacon.observeCategoryGrid(responseId, gridElement);

// Quick add to cart from category grid:
SSBeacon.categoryAddToCart(responseId, [
  { parentId: '123', uid: '456', qty: 1, price: 29.99 }
]);
```

### Autocomplete

[Tracking guide](https://docs.searchspring.com/reference/autocomplete-tracking-guide)

```js
// When autocomplete dropdown renders:
SSBeacon.autocompleteRender(responseId);
var disconnect = SSBeacon.observeAutocompleteGrid(responseId, dropdownElement);

// If SS API returns a redirect:
SSBeacon.autocompleteRedirect(responseId, redirectUrl);

// Quick add from autocomplete dropdown:
SSBeacon.autocompleteAddToCart(responseId, [
  { parentId: '123', uid: '456', qty: 1, price: 29.99 }
]);
```

### Recommendations

[Tracking guide](https://docs.searchspring.com/reference/recommendations-tracking-guide)

All recommendation events take a `tag` that identifies the profile (e.g. `"similar"`, `"trending"`, `"recently-viewed"`).

```js
SSBeacon.recommendationsRender(tag, responseId);
var disconnect = SSBeacon.observeRecommendationsGrid(tag, responseId, gridElement);

// Quick add from recommendations:
SSBeacon.recommendationsAddToCart(tag, responseId, [
  { parentId: '123', uid: '456', qty: 1, price: 49.99 }
]);
```

### Cart

[Tracking guide](https://docs.searchspring.com/reference/cart-add)

After any cart change, pass the fresh Shopify cart object. The beacon diffs it against the last known state to detect adds and removes:

```js
fetch('/cart.js')
  .then(r => r.json())
  .then(data => {
    if (window.SSBeacon) SSBeacon.cartUpdate(data);
  });
```

> **Note:** Shopper login, product pageview, and order transaction events are handled by the SearchSpring Shopify Pixel app — don't duplicate them here.

## API Reference

### Search ([docs](https://docs.searchspring.com/reference/search-tracking-guide))

| Method | Beacon endpoint | Notes |
|--------|-----------------|-------|
| `searchRender(responseId)` | `search/render` | |
| `searchRedirect(responseId, url)` | `search/redirect` | |
| `searchImpression(responseId, items)` | `search/impression` | Manual |
| `searchClick(responseId, items)` | `search/clickthrough` | Manual |
| `observeSearchGrid(responseId, gridEl)` | `search/impression` + `search/clickthrough` | Returns disconnect fn |
| `searchAddToCart(responseId, items)` | `search/addtocart` | |

### Category ([docs](https://docs.searchspring.com/reference/category-tracking-guide))

| Method | Beacon endpoint | Notes |
|--------|-----------------|-------|
| `categoryRender(responseId)` | `category/render` | |
| `categoryImpression(responseId, items)` | `category/impression` | Manual |
| `categoryClick(responseId, items)` | `category/clickthrough` | Manual |
| `observeCategoryGrid(responseId, gridEl)` | `category/impression` + `category/clickthrough` | Returns disconnect fn |
| `categoryAddToCart(responseId, items)` | `category/addtocart` | |

### Autocomplete ([docs](https://docs.searchspring.com/reference/autocomplete-tracking-guide))

| Method | Beacon endpoint | Notes |
|--------|-----------------|-------|
| `autocompleteRender(responseId)` | `autocomplete/render` | |
| `autocompleteRedirect(responseId, url)` | `autocomplete/redirect` | |
| `autocompleteImpression(responseId, items)` | `autocomplete/impression` | Manual |
| `autocompleteClick(responseId, items)` | `autocomplete/clickthrough` | Manual |
| `observeAutocompleteGrid(responseId, gridEl)` | `autocomplete/impression` + `autocomplete/clickthrough` | Returns disconnect fn |
| `autocompleteAddToCart(responseId, items)` | `autocomplete/addtocart` | |

### Recommendations ([docs](https://docs.searchspring.com/reference/recommendations-tracking-guide))

| Method | Beacon endpoint | Notes |
|--------|-----------------|-------|
| `recommendationsRender(tag, responseId)` | `recommendations/render` | |
| `recommendationsImpression(tag, responseId, items)` | `recommendations/impression` | Manual |
| `recommendationsClick(tag, responseId, items)` | `recommendations/clickthrough` | Manual |
| `observeRecommendationsGrid(tag, responseId, gridEl)` | `recommendations/impression` + `recommendations/clickthrough` | Returns disconnect fn |
| `recommendationsAddToCart(tag, responseId, items)` | `recommendations/addtocart` | |

### Cart ([docs](https://docs.searchspring.com/reference/cart-add))

| Method | Beacon endpoint |
|--------|-----------------|
| `cartUpdate(shopifyCart)` | `cart/add` + `cart/remove` |

## How it works

- **Non-blocking**: Beacons fire via `requestIdleCallback` → `navigator.sendBeacon` (`text/plain` to skip CORS preflight)
- **Impressions**: Per-card IntersectionObserver (70 % visible, 1 s dwell). Batched every 500 ms. Deduped per module + tag. MutationObserver catches late-added cards
- **Clicks**: Delegated click handler on the grid — works for static and dynamic cards
- **Cart**: Keeps an internal cart snapshot and diffs on each `cartUpdate()` to find adds/removes
- **Session**: Auto-manages `ss-user-id` (2 yr cookie) and `ss-session-id` (30 min sliding)

## Versioning

Uses [semver](https://semver.org/). The CDN URL `@1` resolves to the latest 1.x release. Pin to `@1.0.0` for an exact version.

## Dev

```bash
npm install
npm run build    # → dist/beacon.js + dist/beacon.min.js
```

## License

MIT