@g2crowd/buyer-intent-provider-sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,336 @@
1
+ import { createBuyerIntentCore } from './core.js';
2
+ import { sendWithBeacon } from './transport.js';
3
+ import { createIdentityManager } from './identity.js';
4
+ import {
5
+ readTags,
6
+ readElementConfig,
7
+ resolveEventName,
8
+ readSessionConfig,
9
+ collectSubjectIds,
10
+ readViewContext,
11
+ } from './dom.js';
12
+
13
+ const DEFAULT_ACTIVITY_ENDPOINT = '/activity/events';
14
+ const DEFAULT_VIEW_NAME = '$view';
15
+ const DEFAULT_CLICK_NAME = '$click';
16
+
17
+ const firedViewElements = new WeakSet();
18
+
19
+ export function createBuyerIntentSDK() {
20
+ const core = createBuyerIntentCore();
21
+ const identity = createIdentityManager();
22
+ let activityEndpoint = DEFAULT_ACTIVITY_ENDPOINT;
23
+ let origin = null;
24
+ let viewName = DEFAULT_VIEW_NAME;
25
+ let userType = 'standard';
26
+ let lastPageKey = null;
27
+ let initialLandingPage = null;
28
+ let initialReferrer = null;
29
+ let initialUtmParams = null;
30
+
31
+ function readUtmParams(urlValue) {
32
+ if (!urlValue) {
33
+ return {};
34
+ }
35
+
36
+ const url = new URL(urlValue, window.location.origin);
37
+ const params = url.searchParams;
38
+
39
+ return {
40
+ utm_source: params.get('utm_source') || undefined,
41
+ utm_medium: params.get('utm_medium') || undefined,
42
+ utm_campaign: params.get('utm_campaign') || undefined,
43
+ utm_term: params.get('utm_term') || undefined,
44
+ utm_content: params.get('utm_content') || undefined,
45
+ };
46
+ }
47
+
48
+ function buildVisitContext() {
49
+ if (typeof window === 'undefined') {
50
+ return {};
51
+ }
52
+
53
+ const landingPage = initialLandingPage || window.location.href;
54
+ return {
55
+ landingPage,
56
+ referrer: initialReferrer || document.referrer || undefined,
57
+ userAgent:
58
+ typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
59
+ utmParams: initialUtmParams || readUtmParams(landingPage),
60
+ };
61
+ }
62
+
63
+ function applyElementConfig(element) {
64
+ const config = readElementConfig(element);
65
+
66
+ if (config.activityEndpoint) {
67
+ setActivityEndpoint(config.activityEndpoint);
68
+ }
69
+
70
+ if (config.origin || config.userType || config.distinctId) {
71
+ setBaseProperties({
72
+ origin: config.origin,
73
+ userType: config.userType,
74
+ distinctId: config.distinctId,
75
+ });
76
+ }
77
+ }
78
+
79
+ function applyDomTags(element) {
80
+ const elements = element ? new Set([element]) : undefined;
81
+ const entries = readTags(elements);
82
+ entries.forEach(({ element: tagElement, tag }) => {
83
+ applyElementConfig(tagElement);
84
+ core.tagPage(tag);
85
+ });
86
+ return entries.length > 0;
87
+ }
88
+
89
+ function sendPayload(element, defaultEventName) {
90
+ if (typeof window === 'undefined') {
91
+ return false;
92
+ }
93
+
94
+ const eventName = resolveEventName(element, defaultEventName);
95
+ const payload = core.buildPayload({
96
+ name: eventName,
97
+ url: window.location.href,
98
+ origin: origin || window.location.hostname,
99
+ distinctId: identity.ensure(),
100
+ userType,
101
+ visit: buildVisitContext(),
102
+ });
103
+
104
+ const sent = sendWithBeacon(activityEndpoint, payload);
105
+ core.resetPageState();
106
+ return sent;
107
+ }
108
+
109
+ function trackPageviewInternal(element) {
110
+ if (typeof window === 'undefined') {
111
+ return false;
112
+ }
113
+
114
+ const pageKey = `${window.location.pathname}${window.location.search}`;
115
+ if (pageKey === lastPageKey) {
116
+ return false;
117
+ }
118
+
119
+ lastPageKey = pageKey;
120
+ return sendPayload(element, viewName);
121
+ }
122
+
123
+ function trackPageview(element) {
124
+ if (typeof window === 'undefined') {
125
+ return false;
126
+ }
127
+
128
+ const pageKey = `${window.location.pathname}${window.location.search}`;
129
+ if (pageKey === lastPageKey) {
130
+ return false;
131
+ }
132
+
133
+ lastPageKey = pageKey;
134
+ const hasTags = applyDomTags(element);
135
+ if (!hasTags) {
136
+ return false;
137
+ }
138
+
139
+ return sendPayload(element, viewName);
140
+ }
141
+
142
+ function trackClick(element) {
143
+ if (typeof window === 'undefined' || !element) {
144
+ return false;
145
+ }
146
+
147
+ return sendPayload(element, DEFAULT_CLICK_NAME);
148
+ }
149
+
150
+ function init(options = {}) {
151
+ activityEndpoint = options.activityEndpoint || activityEndpoint;
152
+ origin = options.origin || origin;
153
+ viewName = options.viewName || options.pageviewName || viewName;
154
+ userType = options.userType || userType;
155
+
156
+ if (options.distinctId) {
157
+ identity.set(options.distinctId);
158
+ }
159
+
160
+ if (typeof window !== 'undefined' && !initialLandingPage) {
161
+ initialLandingPage = window.location.href;
162
+ initialReferrer = document.referrer || undefined;
163
+ initialUtmParams = readUtmParams(initialLandingPage);
164
+ }
165
+
166
+ core.setBaseProperties({
167
+ origin: origin || undefined,
168
+ distinctId: identity.get() || undefined,
169
+ userType,
170
+ });
171
+ }
172
+
173
+ function setActivityEndpoint(value) {
174
+ activityEndpoint = value || activityEndpoint;
175
+ }
176
+
177
+ function setBaseProperties(options = {}) {
178
+ if (options.origin) {
179
+ origin = options.origin;
180
+ }
181
+
182
+ if (options.distinctId) {
183
+ identity.set(options.distinctId);
184
+ }
185
+
186
+ if (options.userType) {
187
+ userType = options.userType;
188
+ }
189
+
190
+ core.setBaseProperties({
191
+ origin: origin || undefined,
192
+ distinctId: identity.get() || undefined,
193
+ userType,
194
+ });
195
+ }
196
+
197
+ function tagPage(options = {}) {
198
+ core.tagPage(options);
199
+ }
200
+
201
+ function setVisitProperties(options = {}) {
202
+ core.setVisitProperties(options);
203
+ }
204
+
205
+ function trackNextRouter(nextRouter) {
206
+ if (!nextRouter || !nextRouter.events) {
207
+ return () => {};
208
+ }
209
+
210
+ const handler = () => trackPageview();
211
+ nextRouter.events.on('routeChangeComplete', handler);
212
+ return () => nextRouter.events.off('routeChangeComplete', handler);
213
+ }
214
+
215
+ function resolveConfig(element) {
216
+ const session = readSessionConfig(element);
217
+ const own = readElementConfig(element);
218
+ return {
219
+ origin: session.origin || own.origin,
220
+ activityEndpoint: session.activityEndpoint || own.activityEndpoint,
221
+ userType: session.userType || own.userType,
222
+ distinctId: session.distinctId || own.distinctId,
223
+ };
224
+ }
225
+
226
+ function applyConfig(config) {
227
+ if (config.activityEndpoint) {
228
+ setActivityEndpoint(config.activityEndpoint);
229
+ }
230
+ if (config.origin || config.userType || config.distinctId) {
231
+ setBaseProperties({
232
+ origin: config.origin,
233
+ userType: config.userType,
234
+ distinctId: config.distinctId,
235
+ });
236
+ }
237
+ }
238
+
239
+ function handleView(element) {
240
+ if (!element || firedViewElements.has(element)) {
241
+ return false;
242
+ }
243
+
244
+ firedViewElements.add(element);
245
+
246
+ // 1. Resolve config: session ancestor > element's own data-* attrs
247
+ const config = resolveConfig(element);
248
+ applyConfig(config);
249
+
250
+ // 2. Read page tags from element's data-buyer-intent (existing flow)
251
+ const entries = readTags(new Set([element]));
252
+ entries.forEach(({ element: tagElement, tag }) => {
253
+ if (tagElement !== element) {
254
+ applyElementConfig(tagElement);
255
+ }
256
+ core.tagPage(tag);
257
+ });
258
+
259
+ // 3. Collect IDs from descendant <buyer-intent-subject> elements
260
+ const subjectIds = collectSubjectIds(element);
261
+ if (subjectIds.productIds.length || subjectIds.categoryIds.length) {
262
+ core.tagPage(subjectIds);
263
+ }
264
+
265
+ // 4. Fire pageview
266
+ return trackPageviewInternal(element, config);
267
+ }
268
+
269
+ function handleClick(element) {
270
+ if (!element) {
271
+ return false;
272
+ }
273
+
274
+ // 1. Resolve config: session ancestor > element's own data-* attrs
275
+ const config = resolveConfig(element);
276
+ applyConfig(config);
277
+
278
+ // 2. Inherit tag/IDs from ancestor <buyer-intent-view>
279
+ const viewCtx = readViewContext(element);
280
+ if (viewCtx && typeof viewCtx === 'object') {
281
+ core.tagPage(viewCtx);
282
+ }
283
+
284
+ // 3. Also read click element's own data-buyer-intent
285
+ const entries = readTags(new Set([element]));
286
+ entries.forEach(({ tag }) => core.tagPage(tag));
287
+
288
+ return trackClick(element);
289
+ }
290
+
291
+ function listenForElements() {
292
+ if (typeof document === 'undefined') {
293
+ return;
294
+ }
295
+
296
+ if (document.__buyerIntentListenersBound) {
297
+ return;
298
+ }
299
+
300
+ document.__buyerIntentListenersBound = true;
301
+
302
+ document.addEventListener('buyerintent:view', (e) => {
303
+ handleView(e.target);
304
+ });
305
+
306
+ document.addEventListener('buyerintent:click', (e) => {
307
+ handleClick(e.target);
308
+ });
309
+ }
310
+
311
+ listenForElements();
312
+
313
+ return {
314
+ init,
315
+ trackPageview,
316
+ trackClick,
317
+ tagPage,
318
+ setBaseProperties,
319
+ setVisitProperties,
320
+ setActivityEndpoint,
321
+ trackNextRouter,
322
+ listenForElements,
323
+ };
324
+ }
325
+
326
+ let _instance;
327
+
328
+ export function ensureSDK() {
329
+ if (!_instance) {
330
+ _instance = createBuyerIntentSDK();
331
+ }
332
+ _instance.listenForElements();
333
+ return _instance;
334
+ }
335
+
336
+ export const buyerIntent = ensureSDK();
@@ -0,0 +1,25 @@
1
+ export function sendWithBeacon(url, payload) {
2
+ if (
3
+ typeof navigator !== 'undefined' &&
4
+ typeof navigator.sendBeacon === 'function'
5
+ ) {
6
+ const blob = new Blob([JSON.stringify(payload)], {
7
+ type: 'application/json',
8
+ });
9
+ const sent = navigator.sendBeacon(url, blob);
10
+ if (sent) {
11
+ return true;
12
+ }
13
+ }
14
+
15
+ if (typeof fetch !== 'undefined') {
16
+ fetch(url, {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body: JSON.stringify(payload),
20
+ keepalive: true,
21
+ });
22
+ }
23
+
24
+ return false;
25
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,126 @@
1
+ export class BuyerIntentSessionElement extends HTMLElement {}
2
+ export class BuyerIntentSubjectElement extends HTMLElement {}
3
+ export class BuyerIntentViewElement extends HTMLElement {}
4
+ export class BuyerIntentClickElement extends HTMLElement {}
5
+
6
+ export type ViewTag =
7
+ | 'products.show'
8
+ | 'products.pricing'
9
+ | 'products.competitors'
10
+ | 'categories.show'
11
+ | 'comparisons.show'
12
+ | 'reviewers.take_survey';
13
+
14
+ export type ClickEventName = '/ad/clicked' | '/leads/create';
15
+
16
+ /**
17
+ * - `guest` — logged-out visitor
18
+ * - `standard` — logged-in user (default)
19
+ * - `vendor-admin` — vendor role viewing their own content
20
+ * - `observer` — internal employee / superuser
21
+ */
22
+ export type UserType = 'guest' | 'standard' | 'vendor-admin' | 'observer';
23
+
24
+ interface BuyerIntentBaseAttrs {
25
+ 'data-origin'?: string;
26
+ 'data-activity-endpoint'?: string;
27
+ 'data-user-type'?: UserType;
28
+ 'data-distinct-id'?: string;
29
+ 'data-buyer-intent'?: string;
30
+ 'product-id'?: string;
31
+ 'product-ids'?: string;
32
+ 'category-id'?: string;
33
+ }
34
+
35
+ export interface BuyerIntentSessionAttrs {
36
+ origin?: string;
37
+ 'activity-endpoint'?: string;
38
+ 'user-type'?: UserType;
39
+ 'distinct-id'?: string;
40
+ }
41
+
42
+ export interface BuyerIntentSubjectAttrs {
43
+ 'product-id'?: string;
44
+ 'product-ids'?: string;
45
+ 'category-id'?: string;
46
+ 'category-ids'?: string;
47
+ }
48
+
49
+ export interface BuyerIntentViewAttrs extends BuyerIntentBaseAttrs {
50
+ tag?: ViewTag | string;
51
+ 'source-location'?: string;
52
+ }
53
+
54
+ export interface BuyerIntentClickAttrs extends BuyerIntentBaseAttrs {
55
+ 'event-name'?: ClickEventName | string;
56
+ }
57
+
58
+ declare global {
59
+ namespace JSX {
60
+ interface IntrinsicElements {
61
+ 'buyer-intent-session': BuyerIntentSessionAttrs &
62
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
63
+ 'buyer-intent-subject': BuyerIntentSubjectAttrs &
64
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
65
+ 'buyer-intent-view': BuyerIntentViewAttrs &
66
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
67
+ 'buyer-intent-click': BuyerIntentClickAttrs &
68
+ React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
69
+ }
70
+ }
71
+ }
72
+
73
+ export type BuyerIntentInitOptions = {
74
+ activityEndpoint?: string;
75
+ origin?: string;
76
+ viewName?: string;
77
+ distinctId?: string;
78
+ userType?: UserType;
79
+ };
80
+
81
+ export type BuyerIntentTagOptions = {
82
+ productIds?: number[];
83
+ categoryIds?: number[];
84
+ tag?: string;
85
+ sourceLocation?: string;
86
+ context?: Record<string, unknown>;
87
+ };
88
+
89
+ export type BuyerIntentBaseOptions = {
90
+ origin?: string;
91
+ distinctId?: string;
92
+ userType?: UserType;
93
+ };
94
+
95
+ export type BuyerIntentVisitProperties = {
96
+ ip?: string;
97
+ referrer?: string;
98
+ landing_page?: string;
99
+ user_agent?: string;
100
+ utm_source?: string;
101
+ utm_medium?: string;
102
+ utm_campaign?: string;
103
+ utm_term?: string;
104
+ utm_content?: string;
105
+ [key: string]: unknown;
106
+ };
107
+
108
+ export type BuyerIntentSDK = {
109
+ init: (options?: BuyerIntentInitOptions) => void;
110
+ trackPageview: (element?: Element) => boolean;
111
+ trackClick: (element: Element) => boolean;
112
+ tagPage: (options?: BuyerIntentTagOptions) => void;
113
+ setBaseProperties: (options?: BuyerIntentBaseOptions) => void;
114
+ setVisitProperties: (options?: BuyerIntentVisitProperties) => void;
115
+ setActivityEndpoint: (endpoint: string) => void;
116
+ trackNextRouter: (nextRouter: {
117
+ events?: {
118
+ on: (event: string, handler: () => void) => void;
119
+ off: (event: string, handler: () => void) => void;
120
+ };
121
+ }) => () => void;
122
+ };
123
+
124
+ export function createBuyerIntentSDK(): BuyerIntentSDK;
125
+ export function ensureSDK(): BuyerIntentSDK;
126
+ export const buyerIntent: BuyerIntentSDK;
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ buyerIntent,
3
+ createBuyerIntentSDK,
4
+ BuyerIntentSessionElement,
5
+ BuyerIntentSubjectElement,
6
+ BuyerIntentViewElement,
7
+ BuyerIntentClickElement,
8
+ } from './browser/index.js';
@@ -0,0 +1,77 @@
1
+ import type * as React from 'react';
2
+ import type { UserType } from '../index';
3
+
4
+ type CommonProps = {
5
+ sourceLocation?: string;
6
+ context?: Record<string, unknown>;
7
+ origin?: string;
8
+ activityEndpoint?: string;
9
+ userType?: UserType;
10
+ distinctId?: string;
11
+ children?: React.ReactNode;
12
+ };
13
+
14
+ export type SessionProviderProps = {
15
+ origin?: string;
16
+ activityEndpoint?: string;
17
+ userType?: UserType;
18
+ distinctId?: string;
19
+ children?: React.ReactNode;
20
+ };
21
+
22
+ export type SubjectTrackerProps = {
23
+ productId?: number;
24
+ categoryId?: number;
25
+ productIds?: number[];
26
+ categoryIds?: number[];
27
+ };
28
+
29
+ export type ViewTrackerProps = CommonProps & {
30
+ tag?: string;
31
+ productId?: number;
32
+ productIds?: number[];
33
+ categoryId?: number;
34
+ };
35
+
36
+ export type ClickTrackerProps = CommonProps & {
37
+ eventName?: string;
38
+ productId?: number;
39
+ productIds?: number[];
40
+ categoryId?: number;
41
+ };
42
+
43
+ type ProductIdViewProps = CommonProps & { productId: number };
44
+ type CategoryViewProps = CommonProps & { categoryId: number };
45
+ type ProductIdsViewProps = CommonProps & { productIds: number[] };
46
+ type ProductIdClickProps = CommonProps & { productId: number };
47
+
48
+ export const SessionProvider: React.FC<SessionProviderProps>;
49
+ export const SubjectTracker: React.FC<SubjectTrackerProps>;
50
+ export const ViewTracker: React.FC<ViewTrackerProps>;
51
+ export const ClickTracker: React.FC<ClickTrackerProps>;
52
+
53
+ export const ProfileView: React.FC<ProductIdViewProps>;
54
+ export const PricingView: React.FC<ProductIdViewProps>;
55
+ export const CompetitorsView: React.FC<ProductIdViewProps>;
56
+ export const CategoryView: React.FC<CategoryViewProps>;
57
+ export const CompareView: React.FC<ProductIdsViewProps>;
58
+ export const WriteReviewView: React.FC<ProductIdViewProps>;
59
+ export const AdClick: React.FC<ProductIdClickProps>;
60
+ export const LeadCreateClick: React.FC<ProductIdClickProps>;
61
+
62
+ export const BuyerIntent: {
63
+ Session: React.FC<SessionProviderProps>;
64
+ Subject: React.FC<SubjectTrackerProps>;
65
+ View: React.FC<ViewTrackerProps>;
66
+ Click: React.FC<ClickTrackerProps>;
67
+ ViewTracker: React.FC<ViewTrackerProps>;
68
+ ClickTracker: React.FC<ClickTrackerProps>;
69
+ ProfileView: React.FC<ProductIdViewProps>;
70
+ PricingView: React.FC<ProductIdViewProps>;
71
+ CompetitorsView: React.FC<ProductIdViewProps>;
72
+ CategoryView: React.FC<CategoryViewProps>;
73
+ CompareView: React.FC<ProductIdsViewProps>;
74
+ WriteReviewView: React.FC<ProductIdViewProps>;
75
+ AdClick: React.FC<ProductIdClickProps>;
76
+ LeadCreateClick: React.FC<ProductIdClickProps>;
77
+ };
@@ -1,36 +1,46 @@
1
- import { ViewTracker, ClickTracker } from './tag_component.js';
1
+ import '../browser/elements/index.js';
2
2
  import {
3
- CompareView,
4
- CompetitorsView,
3
+ SessionProvider,
4
+ SubjectTracker,
5
+ ViewTracker,
6
+ ClickTracker,
5
7
  ProfileView,
6
8
  PricingView,
9
+ CompetitorsView,
7
10
  CategoryView,
11
+ CompareView,
8
12
  WriteReviewView,
9
13
  AdClick,
10
14
  LeadCreateClick,
11
- } from './trackers.js';
15
+ } from '../browser/components/index.js';
12
16
 
13
- export { createBuyerIntentSDK, buyerIntent } from './sdk.js';
14
- export { ViewTracker, ClickTracker };
15
17
  export {
16
- CompareView,
17
- CompetitorsView,
18
+ SessionProvider,
19
+ SubjectTracker,
20
+ ViewTracker,
21
+ ClickTracker,
18
22
  ProfileView,
19
23
  PricingView,
24
+ CompetitorsView,
20
25
  CategoryView,
26
+ CompareView,
21
27
  WriteReviewView,
22
28
  AdClick,
23
29
  LeadCreateClick,
24
30
  };
25
31
 
26
32
  export const BuyerIntent = {
33
+ Session: SessionProvider,
34
+ Subject: SubjectTracker,
35
+ View: ViewTracker,
36
+ Click: ClickTracker,
27
37
  ViewTracker,
28
38
  ClickTracker,
29
- CompareView,
30
- CompetitorsView,
31
39
  ProfileView,
32
40
  PricingView,
41
+ CompetitorsView,
33
42
  CategoryView,
43
+ CompareView,
34
44
  WriteReviewView,
35
45
  AdClick,
36
46
  LeadCreateClick,