@g2crowd/buyer-intent-provider-sdk
v0.5.0
Published
Buyer intent tracking SDK with pageview defaults
Downloads
298
Keywords
Readme
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.
npm install @g2crowd/buyer-intent-provider-sdkQuick Start
Import the SDK, wrap your page in a session, and you're tracking:
<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 |
<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:
<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:
<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:
<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 |
<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:
<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:
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 |
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
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
PARTNER_ID=partner-a KAFKA_BROKERS=broker1:9092,broker2:9092 \
buyer-intent-serverEnvironment 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
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.
PARTNER_ID=partner-a KAFKA_BRIDGE_URL=http://my-bridge:8080 \
buyer-intent-serverOr pass it programmatically:
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):
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)
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)
{
"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:
{
"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.
