npm package: @getcontextual/shopify-sdk
Description: Easy integration for tracking on headless Shopify sites
Latest version: 1.1.4

# @getcontextual/shopify-sdk

A comprehensive tracking SDK for headless Shopify stores. Works with vanilla JavaScript, React, and any SSR framework. Features SSR-safe tracking, and automatic GTM integration.

## Features

- 🚀 **Framework Agnostic**: Works with vanilla JS, React, and SSR frameworks like Next.js
- 🔒 **SSR-Safe**: All tracking methods gracefully handle server-side rendering
- 📦 **Automatic GTM**: Zero-configuration Google Tag Manager integration
- 📊 **Comprehensive Events**: Product views, collections, cart, checkout, search, and more
- 🎨 **TypeScript**: Full TypeScript support with Shopify type definitions
- ⚡ **Auto-Flush**: Automatic event batching for optimal performance
- 🔄 **React Safe**: Handles React Strict Mode and SSR correctly

## Installation

```bash
npm install @getcontextual/shopify-sdk
# or
pnpm add @getcontextual/shopify-sdk
# or
yarn add @getcontextual/shopify-sdk
```

**Peer Dependencies:** React (≥16.8.0) is optional and only needed if using React components.

## Quick Start

### Vanilla JavaScript

```javascript
import { init, trackProductView } from "@getcontextual/shopify-sdk/vanilla";

// Initialize once
init({
  merchantId: "your-store.myshopify.com",
  autoTrackPageView: true,
  gtmContainerId: "GTM-XXXXXXX", // Optional
});

// Track events
trackProductView(productVariant);
```

### React

```tsx
import { TrackerProvider, useTracker } from "@getcontextual/shopify-sdk/react";

function App() {
  return (
    <TrackerProvider config={{ merchantId: "your-store.myshopify.com" }}>
      <ProductPage product={product} />
    </TrackerProvider>
  );
}

function ProductPage({ product }) {
  const { trackProductView } = useTracker();

  useEffect(() => {
    trackProductView(product.variant);
  }, [product.variant, trackProductView]);

  return <div>{product.title}</div>;
}
```

## Vanilla JavaScript API

Perfect for static sites, Vue.js, Svelte, or any framework without React.

### Initialization

```javascript
import { init } from "@getcontextual/shopify-sdk/vanilla";

init({
  merchantId: "your-store.myshopify.com", // Required
  autoTrackPageView: true, // Automatically track page views
  gtmContainerId: "GTM-XXXXXXX", // Optional: GTM container ID
  autoFlush: true, // Auto-flush events (default: true)
  flushInterval: 100, // Flush interval in ms (default: 100)
});
```

### Tracking Events

```javascript
import {
  trackPageView,
  trackProductView,
  trackCollectionView,
  trackAddToCart,
  trackCheckoutStarted,
  trackCheckoutCompleted,
  trackSearch,
  identify,
} from "@getcontextual/shopify-sdk/vanilla";

// Track page view
trackPageView();

// Track product view
trackProductView(productVariant);

// Track collection view
trackCollectionView(collection);

// Track add to cart
trackAddToCart(cartLine);
// Track checkout
trackCheckoutStarted(checkout);
trackCheckoutCompleted(checkout);

// Track search
trackSearch(query, searchResults);

// Identify user
identify({
  email: "user@example.com",
  phone: "+1234567890",
  firstName: "John",
  lastName: "Doe",
});
```

## React API

Uses React Context for dependency injection, making it easy to access tracking functions throughout your app.

### Setup

```tsx
import { TrackerProvider } from "@getcontextual/shopify-sdk/react";

function App() {
  return (
    <TrackerProvider
      config={{
        merchantId: "your-store.myshopify.com",
        gtmContainerId: "GTM-XXXXXXX", // Optional
        autoFlush: true,
        flushInterval: 100,
      }}
    >
      <YourApp />
    </TrackerProvider>
  );
}
```

### Using Hooks

```tsx
import { useTracker } from "@getcontextual/shopify-sdk/react";

function ProductPage({ product }) {
  const { trackProductView } = useTracker();

  useEffect(() => {
    trackProductView(product.variant);
  }, [product.variant, trackProductView]);

  return <div>{product.title}</div>;
}

function AddToCartButton({ cartLine }) {
  const { trackAddToCart } = useTracker();

  const handleClick = () => {
    // Your add to cart logic
    trackAddToCart(cartLine);
  };

  return <button onClick={handleClick}>Add to Cart</button>;
}
```

### Using Components

For automatic tracking when components mount:

```tsx
import {
  ProductTracking,
  CollectionTracking,
  AddToCartTracking,
  CheckoutStartedTracking,
  CheckoutCompletedTracking,
  IdentifyTracking,
  PageViewTracker,
} from "@getcontextual/shopify-sdk/react";

function ProductPage({ product }) {
  return (
    <>
      <ProductTracking productVariant={product.variant} />
      <h1>{product.title}</h1>
    </>
  );
}

function CheckoutPage({ checkout }) {
  return (
    <>
      <CheckoutStartedTracking checkout={checkout} />
      {/* Your checkout UI */}
    </>
  );
}
```

## Nextjs/Server-Side Rendering (SSR) Guide

All tracking methods are SSR-safe and will gracefully no-op on the server-side:

- ✅ Safe to call tracking methods during SSR
- ✅ No errors or warnings on the server
- ✅ Events only tracked on the client-side after hydration
- ✅ Works with Next.js, Remix, and other SSR frameworks

### Initialize Tracker

Create a client component to initialize the tracker:

```tsx
"use client";

import { useEffect, useContext, createContext } from "react";
import {
  init,
  trackPageView,
  trackProductView,
  trackCollectionView,
  trackAddToCart,
  trackCheckoutStarted,
  trackCheckoutCompleted,
  trackSearch,
  identify,
} from "@getcontextual/shopify-sdk/vanilla";

import type { HeadlessShopifyConfig } from "../tracker";

interface TrackerContextValue {
  trackPageView: typeof trackPageView;
  trackProductView: typeof trackProductView;
  trackCollectionView: typeof trackCollectionView;
  trackAddToCart: typeof trackAddToCart;
  trackCheckoutStarted: typeof trackCheckoutStarted;
  trackCheckoutCompleted: typeof trackCheckoutCompleted;
  trackSearch: typeof trackSearch;
  identify: typeof identify;
}

const TrackerContext = createContext<TrackerContextValue | null>(null);

export function TrackerProvider({
  merchantId: string,
}: {
  merchantId: string;
}) {
  useEffect(() => {
    init({ merchantId, autoTrackPageView: false });
  }, [config]);

  const value: TrackerContextValue = {
    trackPageView,
    trackProductView,
    trackCollectionView,
    trackAddToCart,
    trackSearch,
    trackCheckoutStarted,
    trackCheckoutCompleted,
    identify,
  };

  return (
    <TrackerContext.Provider value={value}>{children}</TrackerContext.Provider>
  );
}

export function useTracker() {
  const context = useContext(TrackerContext);
  if (!context) {
    throw new Error("useTracker must be used within a TrackerProvider");
  }
  return context;
}
```

### Automatic Page View Tracker

#### With Page Router

```tsx
// components/page-view-tracker.tsx
"use client";

import { useEffect } from "react";
import { useRouter } from "next/router";
// contextual sdk is global singleton so can be accessed like this
import { trackPageView } from "@getcontextual/shopify-sdk/vanilla";

export function PageViewTracker() {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = () => {
      void trackPageView();
    };

    router.events.on("routeChangeComplete", handleRouteChange);

    return () => {
      router.events.off("routeChangeComplete", handleRouteChange);
    };
  }, [router.events]);

  return null;
}
```

#### With App Router

```tsx
"use client";
// contextual sdk is global singleton so can be accessed like this
import { useEffect, Suspense } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { useTracker } from "./tracker-provider";

function PageViewHandler() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const { trackPageView } = useTracker();

  useEffect(() => {
    trackPageView();
  }, [pathname, searchParams, trackPageView]);

  return null;
}

export function AppRouterPageViewTracker() {
  return (
    <Suspense fallback={null}>
      <PageViewHandler />
    </Suspense>
  );
}
```

```tsx
// app/layout.tsx

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <TrackerProvider config={{ merchantId: "my-store.myshopify.com" }}>
          <AppRouterPageViewTracker />
          {children}
        </TrackerProvider>
      </body>
    </html>
  );
}
```

### Using Components

```tsx
"use client";

import { trackProductView } from "@getcontextual/shopify-sdk/vanilla";

export function ProductTracking({
  productVariant,
}: {
  productVariant: ShopifyProductVariant;
}) {
  const hasTracked = useRef(false);

  useEffect(() => {
    if (!hasTracked.current && productVariant) {
      hasTracked.current = true;
      void trackProductView(productVariant);
    }
  }, [productVariant]);

  return null;
}
```

### Using Hooks

```tsx
// pages/products/[id].tsx

import { useEffect } from "react";
import { trackProductView } from "@getcontextual/shopify-sdk/vanilla";

export default function ProductPage({ product }) {
  useEffect(() => {
    if (product?.variant) {
      void trackProductView(product.variant);
    }
  }, [product]);

  return <div>{product.title}</div>;
}
```

##W Key Points

1. **Always use `'use client'`**: Any file that imports from `@getcontextual/shopify-sdk` must have `'use client'` at the top
2. **SSR-safe**: All tracking functions are SSR-safe and will no-op on the server
3. **Initialize once**: Call `init()` only once, typically in a provider component
4. **Track in useEffect**: Always track events inside `useEffect` to ensure they run on the client-side
5. **Page views**: Track page views when route changes (App Router: `usePathname`/`useSearchParams`, Pages Router: `router.events`)

## Configuration

### HeadlessShopifyConfig

```typescript
interface HeadlessShopifyConfig {
  merchantId: string; // Required: Your Shopify store domain (e.g., 'store.myshopify.com')
  version?: string; // Optional: SDK version (default: '1.0.0')
  autoFlush?: boolean; // Optional: Auto-flush events (default: true)
  flushInterval?: number; // Optional: Flush interval in milliseconds (default: 100)
  gtmContainerId?: string; // Optional: Google Tag Manager container ID
}
```

### VanillaShopifyConfig

Extends `HeadlessShopifyConfig` with:

```typescript
interface VanillaShopifyConfig extends HeadlessShopifyConfig {
  autoTrackPageView: boolean; // Automatically track page views on init
}
```

## TypeScript Support

The package is fully typed. All Shopify types are based on `@shopify/web-pixels-extension` for consistency.

```typescript
import type {
  ShopifyProductVariant,
  ShopifyCartLine,
  ShopifyCheckout,
  ShopifyCollection,
  ShopifySearchResult,
  HeadlessShopifyConfig,
} from "@getcontextual/shopify-sdk";
```

## Google Tag Manager Integration

### Setup

```typescript
init({
  merchantId: "your-store.myshopify.com",
  gtmContainerId: "GTM-XXXXXXX", // Your GTM container ID
});
```

### How It Works

1. **Automatic Loading**: GTM script loads automatically on tracker initialization
2. **Event Pushing**: All events are pushed to `window.dataLayer`
3. **Event Format**: Events follow standard GTM data layer format

### Event Structure

Events are pushed to `dataLayer` as:

```javascript
{
  event: 'product_viewed',
  event_data: {
    merchant_id: 'your-store.myshopify.com',
    event_id: 'unique-event-id',
    // ... product data, user data, etc.
  }
}
```

**Note:** If you don't provide a `gtmContainerId`, the SDK works perfectly without GTM. Events will only be sent to the Contextual backend.

No additional configuration required - it works out of the box!

## API Reference

### Vanilla JavaScript Functions

All functions from `@getcontextual/shopify-sdk/vanilla`:

- `init(config: VanillaShopifyConfig)` - Initialize tracker
- `getTracker()` - Get tracker instance
- `trackPageView()` - Track page view
- `trackProductView(productVariant)` - Track product view
- `trackCollectionView(collection)` - Track collection view
- `trackAddToCart(cartLine)` - Track add to cart
- `trackCheckoutStarted(checkout)` - Track checkout started
- `trackCheckoutCompleted(checkout)` - Track checkout completed
- `trackSearch(query, results)` - Track search
- `identify(userProperties)` - Identify user

### React Hooks

From `@getcontextual/shopify-sdk/react`:

- `useTracker()` - Access all tracker functions from context

### React Components

From `@getcontextual/shopify-sdk/react`:

- `<TrackerProvider config={config}>` - Provider component
- `<ProductTracking productVariant={variant} />` - Track product views
- `<CollectionTracking collection={collection} />` - Track collection views
- `<AddToCartTracking cartLine={cartLine} />` - Track add to cart
- `<CheckoutStartedTracking checkout={checkout} />` - Track checkout started
- `<CheckoutCompletedTracking checkout={checkout} />` - Track checkout completed
- `<IdentifyTracking userProperties={props} />` - Identify user
- `<PageViewTracker pathname={pathname} />` - Track page views
- `<SearchTracking search={search}, searchResult={searchResult}>` - Track search results

## Examples

### Complete Vanilla JS Example

```javascript
import {
  init,
  trackProductView,
  trackAddToCart,
  identify,
} from "@getcontextual/shopify-sdk/vanilla";

// Initialize
init({
  merchantId: "my-store.myshopify.com",
  autoTrackPageView: true,
  gtmContainerId: "GTM-XXXXXXX",
});

// Product page
function onProductPageLoad(product) {
  trackProductView(product.variant);
}

// Add to cart button
function onAddToCart(cartLine) {
  trackAddToCart(cartLine);
}

// User login
function onUserLogin(user) {
  identify({
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,
  });
}
```

### Complete React Example

```tsx
import {
  TrackerProvider,
  useTracker,
  ProductTracking,
} from "@getcontextual/shopify-sdk/react";

function App() {
  return (
    <TrackerProvider config={{ merchantId: "my-store.myshopify.com" }}>
      <Router>
        <Routes>
          <Route path="/products/:id" element={<ProductPage />} />
          <Route path="/cart" element={<CartPage />} />
        </Routes>
      </Router>
    </TrackerProvider>
  );
}

function ProductPage() {
  const { product } = useLoaderData();
  const { trackAddToCart } = useTracker();

  return (
    <>
      <ProductTracking productVariant={product.variant} />
      <div>
        <h1>{product.title}</h1>
        <button onClick={() => trackAddToCart(product.cartLine)}>
          Add to Cart
        </button>
      </div>
    </>
  );
}
```

## Package Exports

- **`@getcontextual/shopify-sdk`** - Core tracker class and types
- **`@getcontextual/shopify-sdk/vanilla`** - Vanilla JavaScript API
- **`@getcontextual/shopify-sdk/react`** - React hooks and components

## Troubleshooting

### Events Not Being Tracked

1. Ensure `init()` or `<TrackerProvider>` is called before tracking
2. Check browser console for initialization warnings
3. Verify `merchantId` matches your Shopify store domain
4. If using SSR, ensure tracking is called in `useEffect` or after hydration

### GTM Events Not Appearing

1. Verify `gtmContainerId` is correct
2. Inspect `window.dataLayer` in browser console
3. Use GTM preview mode to see if events are received
4. Check Network tab for GTM script requests

### TypeScript Errors

1. Ensure TypeScript version is compatible (^5.9.2)
2. Use the correct import path for your framework
3. Types are automatically included - no additional setup needed

### React Strict Mode

The SDK is React Strict Mode safe. You may see duplicate event tracking in development mode, but this won't happen in production.

## License

Licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE) for details.

## Support

For issues, questions, or contributions, please open an issue on the repository.