npm package: @shopkit/events
Description: Event-driven analytics system for OpenStore storefronts
Latest version: 0.9.0

# @shopkit/events

Event-driven analytics system for e-commerce storefronts.

## Features

- **20 standardized events** aligned with GA4 enhanced e-commerce
- **Type-safe EventBus** with middleware pipeline and ring buffer
- **5-stage middleware** — schema validation, deduplication, user enrichment, dev logging, server relay
- **Cart integration** via `onCartEvent` callback (zero changes to `@shopkit/cart`)
- **Automatic page views** with page type detection
- **Product view tracking** with 20s engagement timer
- **Data mappers** for cart, product, and order data (with priceDivisor for paise→rupees conversion)
- **sendBeacon batching** with fetch keepalive fallback
- **PII hashing** via Web Crypto API (deferred, non-blocking)
- **Performance budget** of 3.2ms per event end-to-end

## Installation

```bash
npm install @shopkit/events
# or
bun add @shopkit/events
```

**Peer dependencies:** `react`, `zod`, `next` (optional), `zustand` (optional)

## Quick Start

### 1. Add EventProvider in your root layout

```tsx
// app/layout.tsx
'use client';

import { EventProvider } from '@shopkit/events/react';
import { usePathname, useSearchParams } from 'next/navigation';

function EventProviderWrapper({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const searchParams = useSearchParams()?.toString();

  return (
    <EventProvider pathname={pathname} searchParams={searchParams}>
      {children}
    </EventProvider>
  );
}

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <EventProviderWrapper>{children}</EventProviderWrapper>
      </body>
    </html>
  );
}
```

### 2. Wire cart events

```typescript
// bootstrap/cart.ts
import { eventBus } from '@shopkit/events';
import { createCartMapper } from '@shopkit/events/mappers';
import { createCartEventHandler } from '@shopkit/events/emitters';
import { configureCart } from '@shopkit/cart';

// priceDivisor: 100 converts paise (35500) to rupees (355.00)
const mapper = createCartMapper('INR', 100);
const handleCartEvent = createCartEventHandler(eventBus, mapper);

configureCart({
  // ...existing config...
  onCartEvent: handleCartEvent,
});
```

### 3. Track product views

```tsx
// components/ProductPage.tsx
import { useProductViewEmitter } from '@shopkit/events/emitters';
import { useEventBus } from '@shopkit/events/react';

function ProductPage({ product }) {
  const bus = useEventBus();
  const standardItem = {
    item_id: product.id,
    item_name: product.title,
    item_brand: product.vendor,
    item_category: product.productType,
    price: product.price,
    quantity: 1,
    currency: 'INR',
  };

  useProductViewEmitter(bus, standardItem);

  return <div>{/* product UI */}</div>;
}
```

### 4. Emit events manually

```tsx
import { useTrackEvent } from '@shopkit/events/react';
import { OpenStoreEventType } from '@shopkit/events';

function SearchResults({ term, count }) {
  const emit = useTrackEvent();

  useEffect(() => {
    emit(OpenStoreEventType.SEARCH, {
      search_term: term,
      results_count: count,
    });
  }, [term, count]);

  return <div>{/* results */}</div>;
}
```

## API Reference

### Core

#### `eventBus`

Singleton `EventBus` instance. The single point through which all analytics events flow.

| Method | Signature | Description |
|--------|-----------|-------------|
| `subscribe` | `(type: OpenStoreEventType, handler) => Unsubscribe` | Subscribe to a specific event type |
| `subscribeAll` | `(handler) => Unsubscribe` | Subscribe to all events |
| `emit` | `(type, data, overrides?) => OpenStoreEvent \| null` | Emit an event (null if blocked by middleware) |
| `use` | `(middleware: Middleware) => void` | Register middleware |
| `getEventLog` | `() => ReadonlyArray<OpenStoreEvent>` | Get ring buffer of last 100 events |
| `reset` | `() => void` | Clear all state (testing only) |

### Event Types

```typescript
import { OpenStoreEventType } from '@shopkit/events';

// 16 client-side events
OpenStoreEventType.PAGE_VIEW
OpenStoreEventType.VIEW_PRODUCT
OpenStoreEventType.ENGAGE_CONTENT
OpenStoreEventType.ADD_TO_CART
OpenStoreEventType.REMOVE_FROM_CART
OpenStoreEventType.VIEW_CART
OpenStoreEventType.BEGIN_CHECKOUT
OpenStoreEventType.ADD_PAYMENT_INFO
OpenStoreEventType.ADD_SHIPPING_INFO
OpenStoreEventType.PURCHASE
OpenStoreEventType.SEARCH
OpenStoreEventType.ADD_TO_WISHLIST
OpenStoreEventType.KWIKPASS_LOGIN_ATTEMPTED
OpenStoreEventType.KWIKPASS_LOGIN_COMPLETED
OpenStoreEventType.VIEW_PROMO
OpenStoreEventType.AB_TEST_VIEWED

// 4 server-side events
OpenStoreEventType.ORDER_FULFILLED
OpenStoreEventType.ORDER_SHIPPED
OpenStoreEventType.ORDER_DELIVERED
OpenStoreEventType.ORDER_CANCELLED
```

### Middleware (`@shopkit/events/middleware`)

| Middleware | Factory | Description |
|-----------|---------|-------------|
| `schemaValidatorMiddleware` | `createSchemaValidatorMiddleware()` | Validates payloads against Zod schemas |
| `deduplicatorMiddleware` | `createDeduplicatorMiddleware(windowMs?)` | Blocks duplicate events within 2s window |
| `userEnricherMiddleware` | `createUserEnricherMiddleware()` | Enriches with cookies, affiliate data, PII hashes |
| `devLoggerMiddleware` | `createDevLoggerMiddleware(options?)` | Console logging (dev default, env/console toggle) |
| `serverRelayMiddleware` | `createServerRelayMiddleware(config?)` | Batches and sends via sendBeacon |

#### Dev Logger Config

```typescript
interface DevLoggerOptions {
  enabled?: boolean; // Explicitly enable/disable. Default: auto (dev mode only)
}
```

Logging is active when ANY of these is true:
1. `enabled: true` (e.g., `process.env.NEXT_PUBLIC_EVENT_DEBUG === "true"`)
2. `NODE_ENV === "development"` (default, unless `enabled: false`)
3. `window.__shopkit_debug = true` (ad-hoc toggle from browser console — works in production)

#### Server Relay Config

```typescript
interface ServerRelayConfig {
  ingestUrl?: string;        // Default: /api/events/ingest
  flushIntervalMs?: number;  // Default: 5000
  maxBatchSize?: number;     // Default: 10
}
```

### Emitters (`@shopkit/events/emitters`)

#### `createCartEventHandler(bus, mapper, options?)`

Creates a handler for `@shopkit/cart`'s `onCartEvent` callback.

```typescript
interface CartEmitterOptions {
  getCartItems?: () => CartItemLike[];
  productIdentifier?: ProductIdentifier; // Override bus.productIdentifier
}
```

The handler normalizes raw cart store items (snake_case fields, price as number) automatically. `productIdentifier` is read lazily from `bus.productIdentifier` at event-fire time, so it respects config set by `EventProvider` even if the cart is bootstrapped first.

#### `<PageEmitter bus={bus} pathname={pathname} searchParams={searchParams} />`

React component that emits `page_view` on route changes. Rendered automatically by `EventProvider`.

#### `useProductViewEmitter(bus, product, options?)`

Hook that emits `view_product` on mount and `engage_content` after 20s dwell time.

### Mappers (`@shopkit/events/mappers`)

| Mapper | Factory | Description |
|--------|---------|-------------|
| `CartMapper` | `createCartMapper(currency?, priceDivisor?)` | CartItem to ProductFields. `priceDivisor` converts raw price units (e.g. 100 for paise→rupees) |
| `ShopifyProductMapper` | `createShopifyProductMapper(currency?)` | Product GraphQL to StandardItem |
| `ShopifyOrderMapper` | `createShopifyOrderMapper(currency?)` | Order to PurchasePayload |

> **Note:** `ShopifyCartMapper` and `createShopifyCartMapper` are deprecated aliases for `CartMapper` and `createCartMapper`.

### React (`@shopkit/events/react`)

#### `<EventProvider config? pathname? searchParams?>`

Root provider. Initializes EventBus with middleware pipeline and renders PageEmitter.

```typescript
interface EventProviderConfig {
  enableDevTools?: boolean;
  ingestUrl?: string;
  disablePageView?: boolean;
  disableServerRelay?: boolean;
  productIdentifier?: ProductIdentifier; // "product_id" | "sku" | "variant_id"
  bus?: EventBus;  // For testing
}
```

#### Hooks

| Hook | Return Type | Description |
|------|-------------|-------------|
| `useEventBus()` | `EventBus` | Access EventBus from context |
| `useTrackEvent()` | `(type, data) => OpenStoreEvent \| null` | Stable emit function |
| `useEventSubscribe(type, handler)` | `void` | Subscribe with auto-cleanup |
| `useEventSubscribeAll(handler)` | `void` | Subscribe to all events |
| `useEventLog()` | `ReadonlyArray<OpenStoreEvent>` | Access event ring buffer |

### Schemas (`@shopkit/events/schemas`)

Zod validation schemas for all 20 events plus shared data shapes.

| Export | Description |
|--------|-------------|
| `StandardItemSchema` | 15-field GA4-aligned item schema |
| `StandardUserDataSchema` | PII hashed user data schema |
| `StandardCookiesSchema` | Platform tracking cookies schema |
| `eventPayloadSchemas` | Record mapping event type to Zod schema |
| `OpenStoreEventEnvelopeSchema` | Full event envelope structural schema |

## Sub-path Exports

```typescript
import { eventBus, OpenStoreEventType } from '@shopkit/events';
import { schemaValidatorMiddleware } from '@shopkit/events/middleware';
import { createCartEventHandler } from '@shopkit/events/emitters';
import { createCartMapper } from '@shopkit/events/mappers';
import { EventProvider, useTrackEvent } from '@shopkit/events/react';
import { eventPayloadSchemas } from '@shopkit/events/schemas';
```

## License

MIT