npm package: @g2crowd/buyer-intent-provider-sdk
Description: Buyer intent tracking SDK with pageview defaults
Latest version: 0.5.0

# Buyer Intent JS SDK

Buyer intent tracking for partner sites. Drop custom HTML elements into any page — no framework required. Events are sent via `sendBeacon` for reliable delivery during navigation.

```bash
npm install @g2crowd/buyer-intent-provider-sdk
```

## Quick Start

Import the SDK, wrap your page in a session, and you're tracking:

```html
<script type="module">
  import '@g2crowd/buyer-intent-provider-sdk';
</script>

<buyer-intent-session
  origin="yoursite.com"
  activity-endpoint="/api/activity/events"
>

  <buyer-intent-view tag="products.show">
    <buyer-intent-subject product-id="123"></buyer-intent-subject>

    <h1>Acme CRM</h1>
    <p>Product details here.</p>

    <buyer-intent-click event-name="/leads/create">
      <button>Get a Demo</button>
    </buyer-intent-click>
  </buyer-intent-view>

</buyer-intent-session>
```

That's it. When the page loads, a `$view` event fires with `product_ids: [123]` and `tag: "products.show"`. When the user clicks "Get a Demo", a `/leads/create` event fires. Both events inherit the session's `origin` and `activity-endpoint` automatically.

## Elements

### `<buyer-intent-session>`

Wraps a page (or your entire app) and provides config to all elements inside it. Does not fire any events itself.

| Attribute            | Description                                  |
| -------------------- | -------------------------------------------- |
| `origin`             | Partner hostname                             |
| `activity-endpoint`  | URL to POST events to                        |
| `user-type`          | Visitor role (default: `"standard"`)         |
| `distinct-id`        | Override the auto-generated visitor ID       |

| User Type        | Meaning                                           |
| ---------------- | ------------------------------------------------- |
| `guest`          | Logged-out visitor                                |
| `standard`       | Logged-in user (default)                          |
| `vendor-admin`   | Vendor role viewing their own content             |
| `observer`       | Internal employee / superuser                     |

```html
<buyer-intent-session origin="yoursite.com" activity-endpoint="/api/activity/events">
  <!-- everything inside inherits origin and endpoint -->
</buyer-intent-session>
```

### `<buyer-intent-view>`

Fires a `$view` event when the element connects to the DOM. One event per page navigation.

| Attribute         | Description                                 |
| ----------------- | ------------------------------------------- |
| `tag`             | View type identifier (optional)             |
| `source-location` | Controller/action string (optional)         |
| `product-id`      | Single product ID (simple pages)            |
| `category-id`     | Single category ID (simple pages)           |

For pages with one product, you can put the ID directly on the view:

```html
<buyer-intent-view tag="products.show" product-id="123">
  <h1>Acme CRM</h1>
</buyer-intent-view>
```

For pages with multiple products, use `<buyer-intent-subject>` children instead (see below).

### `<buyer-intent-subject>`

Declares that a product or category is a **subject** of the current view. Nest any number of these inside a `<buyer-intent-view>` — their IDs accumulate into the view's event automatically. Does not render anything visible.

| Attribute      | Description                                          |
| -------------- | ---------------------------------------------------- |
| `product-id`   | Single product ID                                    |
| `product-ids`  | Multiple product IDs (JSON array or comma-separated) |
| `category-id`  | Single category ID                                   |
| `category-ids` | Multiple category IDs (JSON array or comma-separated)|

A comparison page that references two products:

```html
<buyer-intent-view tag="comparisons.show">
  <buyer-intent-subject product-id="101"></buyer-intent-subject>
  <buyer-intent-subject product-id="201"></buyer-intent-subject>

  <h1>Acme CRM vs. Rival CRM</h1>
</buyer-intent-view>
```

The resulting event has `product_ids: [101, 201]`. If the view also has an inline `product-id`, they merge together without duplicates.

Subjects can live anywhere inside the view — they don't need to be direct children. This makes them easy to place next to the content they describe:

```html
<buyer-intent-view tag="categories.show">
  <buyer-intent-subject category-id="45"></buyer-intent-subject>
  <h1>CRM Software</h1>

  <div class="product-grid">
    <div class="product-card">
      <buyer-intent-subject product-id="101"></buyer-intent-subject>
      <h2>Acme CRM</h2>
    </div>

    <div class="product-card">
      <buyer-intent-subject product-id="201"></buyer-intent-subject>
      <h2>Rival CRM</h2>
    </div>
  </div>
</buyer-intent-view>
```

This fires one event: `category_ids: [45]`, `product_ids: [101, 201]`, `tag: "categories.show"`.

### `<buyer-intent-click>`

Fires an event when the user clicks anything inside it.

| Attribute    | Description              |
| ------------ | ------------------------ |
| `event-name` | Click event type         |

```html
<buyer-intent-click event-name="/leads/create">
  <button>Get a Demo</button>
</buyer-intent-click>
```

A click element inside a view automatically inherits the view's tag, product IDs, and category IDs. A click inside a session inherits the session's config. You don't need to repeat anything.

## Full Example

Putting it all together — a product page with a session, view, subjects, and two click actions:

```html
<script type="module">
  import '@g2crowd/buyer-intent-provider-sdk';
</script>

<buyer-intent-session
  origin="yoursite.com"
  activity-endpoint="/api/activity/events"
  user-type="standard"
>

  <buyer-intent-view
    tag="products.show"
    source-location="ProductsController#show"
  >
    <buyer-intent-subject product-id="123"></buyer-intent-subject>
    <buyer-intent-subject category-id="45"></buyer-intent-subject>

    <h1>Acme CRM</h1>
    <p>The best CRM for small teams.</p>

    <buyer-intent-click event-name="/leads/create">
      <button>Get a Demo</button>
    </buyer-intent-click>

    <buyer-intent-click event-name="/ad/clicked">
      <a href="https://acme.example.com">Visit Acme</a>
    </buyer-intent-click>
  </buyer-intent-view>

</buyer-intent-session>
```

## React Support

Import from `/react` and use the same element hierarchy with camelCase props:

```tsx
import { BuyerIntent } from '@g2crowd/buyer-intent-provider-sdk/react';

export default function ProductPage() {
  return (
    <BuyerIntent.Session origin="yoursite.com" activityEndpoint="/api/activity/events">
      <BuyerIntent.View tag="products.show" sourceLocation="ProductsController#show">
        <BuyerIntent.Subject productId={123} />
        <BuyerIntent.Subject categoryId={45} />

        <h1>Acme CRM</h1>

        <BuyerIntent.Click eventName="/leads/create">
          <button>Get a Demo</button>
        </BuyerIntent.Click>
      </BuyerIntent.View>
    </BuyerIntent.Session>
  );
}
```

`BuyerIntent.Session`, `BuyerIntent.View`, `BuyerIntent.Subject`, and `BuyerIntent.Click` map 1:1 to the HTML elements described above. The session is a good fit for a layout component or `_app` wrapper so it's set once for every page.

## Syntactic Sugar

For common page types, pre-built components hardcode the `tag` or `event-name` so you don't have to:

### Views

| Component         | Tag                     | Required Prop |
| ----------------- | ----------------------- | ------------- |
| `ProfileView`     | `products.show`         | `productId`   |
| `PricingView`     | `products.pricing`      | `productId`   |
| `CompetitorsView` | `products.competitors`  | `productId`   |
| `CategoryView`    | `categories.show`       | `categoryId`  |
| `CompareView`     | `comparisons.show`      | `productIds`  |
| `WriteReviewView` | `reviewers.take_survey` | `productId`   |

### Clicks

| Component         | Event Name      | Required Prop |
| ----------------- | --------------- | ------------- |
| `AdClick`         | `/ad/clicked`   | `productId`   |
| `LeadCreateClick` | `/leads/create` | `productId`   |

```tsx
import { BuyerIntent } from '@g2crowd/buyer-intent-provider-sdk/react';

<BuyerIntent.Session origin="yoursite.com" activityEndpoint="/api/activity/events">
  <BuyerIntent.ProfileView productId={123}>
    <h1>Acme CRM</h1>

    <BuyerIntent.LeadCreateClick productId={123}>
      <button>Get a Demo</button>
    </BuyerIntent.LeadCreateClick>
  </BuyerIntent.ProfileView>
</BuyerIntent.Session>
```

These components still accept `origin`, `activityEndpoint`, `sourceLocation`, and `context` props for cases where you aren't using a session wrapper.

## Backend

### Next.js Route Handler

```ts
import { createNextRouteHandler } from '@g2crowd/buyer-intent-provider-sdk/server';

const POST = createNextRouteHandler({
  kafkaBrokers: [process.env.KAFKA_BROKER || 'localhost:9092'],
  partnerId: process.env.PARTNER_ID,
});

export { POST };
```

### Standalone Server

```bash
PARTNER_ID=partner-a KAFKA_BROKERS=broker1:9092,broker2:9092 \
buyer-intent-server
```

Environment variables: `PORT` (default 3000), `PARTNER_ID`, `KAFKA_BROKERS` (comma-separated), `KAFKA_BRIDGE_URL` (Strimzi HTTP bridge), `KAFKA_BRIDGE_USERNAME`, `KAFKA_BRIDGE_PASSWORD`, `TOPIC_PREFIX`, `KAFKA_TOPIC`, `KAFKA_CLIENT_ID`, `USE_DEV_LOGGER=true`.

### Custom Kafka Producer

```ts
import {
  createNextRouteHandler,
  createKafkaProducer,
} from '@g2crowd/buyer-intent-provider-sdk/server';

const POST = createNextRouteHandler({
  producer: createKafkaProducer({
    brokers: (process.env.KAFKA_BROKERS || '').split(','),
    clientId: 'partner-app',
  }),
  partnerId: process.env.PARTNER_ID,
});

export { POST };
```

You can also pass a raw `kafkajs` producer or any object with a `send({ topic, messages })` method.

### HTTP Bridge (Strimzi)

Use `KAFKA_BRIDGE_URL` to produce via an HTTP bridge instead of connecting to Kafka directly. No `kafkajs` dependency required.

```bash
PARTNER_ID=partner-a KAFKA_BRIDGE_URL=http://my-bridge:8080 \
buyer-intent-server
```

Or pass it programmatically:

```ts
import { createNextRouteHandler } from '@g2crowd/buyer-intent-provider-sdk/server';

const POST = createNextRouteHandler({
  kafkaBridgeUrl: process.env.KAFKA_BRIDGE_URL,
  partnerId: process.env.PARTNER_ID,
});

export { POST };
```

For custom headers (e.g. auth tokens):

```ts
import {
  createNextRouteHandler,
  createHttpProducer,
} from '@g2crowd/buyer-intent-provider-sdk/server';

const POST = createNextRouteHandler({
  producer: createHttpProducer({
    bridgeUrl: process.env.KAFKA_BRIDGE_URL,
    headers: { Authorization: `Bearer ${process.env.BRIDGE_TOKEN}` },
  }),
  partnerId: process.env.PARTNER_ID,
});

export { POST };
```

### Dev Adapter (No Kafka)

```ts
import {
  createNextRouteHandler,
  createDevLoggerProducer,
} from '@g2crowd/buyer-intent-provider-sdk/server';

const POST = createNextRouteHandler({
  producer: createDevLoggerProducer(),
  partnerId: 'dev',
});

export { POST };
```

## Event Payload

The client sends a beacon to the activity endpoint. The server handler wraps it into a composite event and writes it to Kafka.

### Client → Server (sendBeacon body)

```json
{
  "name": "$view",
  "properties": {
    "product_ids": [123],
    "category_ids": [45],
    "tag": "products.show",
    "url": "https://yoursite.com/products/acme-crm",
    "user_type": "standard",
    "distinct_id": "visitor-uuid",
    "origin": "yoursite.com",
    "source_location": "ProductsController#show",
    "context": {}
  },
  "visit": {
    "properties": {
      "landing_page": "https://yoursite.com/products/acme-crm",
      "referrer": "https://google.com/",
      "user_agent": "Mozilla/5.0 ...",
      "utm_source": "newsletter",
      "utm_medium": "email",
      "utm_campaign": "spring-2026",
      "utm_term": "crm+software",
      "utm_content": "hero-cta"
    }
  }
}
```

### Server → Kafka (composite event)

The server enriches the client payload with server-side tokens, timestamps, and IP before writing to Kafka. This is the shape written to the `intent_events_{partnerId}` topic:

```json
{
  "event": {
    "id": "e0c1f2a3-...",
    "name": "$view",
    "time": "2026-02-17T14:20:00.000Z",
    "properties": {
      "product_ids": [123],
      "category_ids": [45],
      "tag": "products.show",
      "url": "https://yoursite.com/products/acme-crm",
      "user_type": "standard",
      "distinct_id": "visitor-uuid",
      "origin": "yoursite.com",
      "source_location": "ProductsController#show",
      "context": {}
    }
  },
  "visit": {
    "visit_token": "a1b2c3d4-...",
    "visitor_token": "f5e6d7c8-...",
    "started_at": "2026-02-17T14:20:00.000Z",
    "created_at": "2026-02-17T14:20:00.000Z",
    "properties": {
      "landing_page": "https://yoursite.com/products/acme-crm",
      "referrer": "https://google.com/",
      "user_agent": "Mozilla/5.0 ...",
      "ip": "203.0.113.42",
      "utm_source": "newsletter",
      "utm_medium": "email",
      "utm_campaign": "spring-2026",
      "utm_term": "crm+software",
      "utm_content": "hero-cta"
    }
  }
}
```

The server sets `visit_token` and `visitor_token` as `httpOnly` cookies so repeat visits from the same browser share stable tokens.