@g2crowd/buyer-intent-provider-sdk 0.3.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,184 @@
1
+ function parseJson(value) {
2
+ if (!value) {
3
+ return undefined;
4
+ }
5
+
6
+ try {
7
+ return JSON.parse(value);
8
+ } catch (_error) {
9
+ return undefined;
10
+ }
11
+ }
12
+
13
+ function parseArray(value) {
14
+ if (!value) {
15
+ return undefined;
16
+ }
17
+
18
+ const parsed = parseJson(value);
19
+ if (Array.isArray(parsed)) {
20
+ return parsed;
21
+ }
22
+
23
+ if (typeof value === 'string') {
24
+ return value
25
+ .split(',')
26
+ .map((entry) => entry.trim())
27
+ .filter(Boolean);
28
+ }
29
+
30
+ return undefined;
31
+ }
32
+
33
+ function readTagElement(element) {
34
+ if (!element) {
35
+ return undefined;
36
+ }
37
+
38
+ if (element.tagName === 'SCRIPT') {
39
+ const parsed =
40
+ parseJson(element.textContent || '') ||
41
+ parseJson(element.getAttribute('data-buyer-intent'));
42
+ if (parsed && typeof parsed === 'object') {
43
+ return parsed;
44
+ }
45
+ }
46
+
47
+ const dataJson = parseJson(element.getAttribute('data-buyer-intent'));
48
+ if (dataJson && typeof dataJson === 'object') {
49
+ return dataJson;
50
+ }
51
+
52
+ const dataset = element.dataset || {};
53
+ return {
54
+ productIds: parseArray(dataset.productIds),
55
+ categoryIds: parseArray(dataset.categoryIds),
56
+ tag: dataset.tag,
57
+ sourceLocation: dataset.sourceLocation,
58
+ context: parseJson(dataset.context),
59
+ };
60
+ }
61
+
62
+ export function readTags(elements) {
63
+ if (typeof document === 'undefined') {
64
+ return [];
65
+ }
66
+
67
+ const found = elements || new Set();
68
+ const legacy = document.getElementById('buyer-intent-tags');
69
+ if (legacy) {
70
+ found.add(legacy);
71
+ }
72
+
73
+ document.querySelectorAll('[data-buyer-intent]').forEach((element) => {
74
+ found.add(element);
75
+ });
76
+
77
+ return Array.from(found)
78
+ .map((element) => ({ element, tag: readTagElement(element) }))
79
+ .filter((entry) => entry.tag && typeof entry.tag === 'object');
80
+ }
81
+
82
+ export function readElementConfig(element) {
83
+ if (!element || !element.dataset) {
84
+ return {};
85
+ }
86
+
87
+ const { origin, userType, distinctId, activityEndpoint } = element.dataset;
88
+
89
+ return { origin, userType, distinctId, activityEndpoint };
90
+ }
91
+
92
+ export function resolveEventName(element, fallback) {
93
+ if (element && element.dataset && element.dataset.eventName) {
94
+ return element.dataset.eventName;
95
+ }
96
+
97
+ return fallback;
98
+ }
99
+
100
+ function parseIds(raw) {
101
+ if (!raw) return [];
102
+ try {
103
+ const parsed = JSON.parse(raw);
104
+ if (Array.isArray(parsed)) return parsed.map(Number);
105
+ } catch (_e) {}
106
+ return raw.split(',').map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n));
107
+ }
108
+
109
+ export function readSessionConfig(element) {
110
+ if (!element || typeof element.closest !== 'function') {
111
+ return {};
112
+ }
113
+
114
+ const session = element.closest('buyer-intent-session');
115
+ if (!session) {
116
+ return {};
117
+ }
118
+
119
+ return {
120
+ origin: session.getAttribute('origin') || undefined,
121
+ activityEndpoint: session.getAttribute('activity-endpoint') || undefined,
122
+ userType: session.getAttribute('user-type') || undefined,
123
+ distinctId: session.getAttribute('distinct-id') || undefined,
124
+ };
125
+ }
126
+
127
+ export function collectSubjectIds(viewElement) {
128
+ if (!viewElement || typeof viewElement.querySelectorAll !== 'function') {
129
+ return { productIds: [], categoryIds: [] };
130
+ }
131
+
132
+ const subjects = viewElement.querySelectorAll('buyer-intent-subject');
133
+ const productIds = [];
134
+ const categoryIds = [];
135
+
136
+ for (const subject of subjects) {
137
+ const pids = subject.getAttribute('product-ids');
138
+ const pid = subject.getAttribute('product-id');
139
+ if (pids) {
140
+ productIds.push(...parseIds(pids));
141
+ } else if (pid) {
142
+ productIds.push(Number(pid));
143
+ }
144
+
145
+ const cids = subject.getAttribute('category-ids');
146
+ const cid = subject.getAttribute('category-id');
147
+ if (cids) {
148
+ categoryIds.push(...parseIds(cids));
149
+ } else if (cid) {
150
+ categoryIds.push(Number(cid));
151
+ }
152
+ }
153
+
154
+ return { productIds, categoryIds };
155
+ }
156
+
157
+ export function readViewContext(clickElement) {
158
+ if (!clickElement || typeof clickElement.closest !== 'function') {
159
+ return {};
160
+ }
161
+
162
+ const view = clickElement.closest('buyer-intent-view');
163
+ if (!view) {
164
+ return {};
165
+ }
166
+
167
+ const viewData = readTagElement(view) || {};
168
+ const subjectIds = collectSubjectIds(view);
169
+
170
+ const productIds = [
171
+ ...(viewData.productIds || []),
172
+ ...subjectIds.productIds,
173
+ ];
174
+ const categoryIds = [
175
+ ...(viewData.categoryIds || []),
176
+ ...subjectIds.categoryIds,
177
+ ];
178
+
179
+ return {
180
+ ...viewData,
181
+ productIds: productIds.length ? [...new Set(productIds)] : undefined,
182
+ categoryIds: categoryIds.length ? [...new Set(categoryIds)] : undefined,
183
+ };
184
+ }
@@ -33,6 +33,11 @@ function readIntentData(element) {
33
33
  data.tag = tag;
34
34
  }
35
35
 
36
+ const sourceLocation = element.getAttribute('source-location');
37
+ if (sourceLocation) {
38
+ data.sourceLocation = sourceLocation;
39
+ }
40
+
36
41
  const eventName = element.getAttribute('event-name');
37
42
  if (eventName) {
38
43
  element.dataset.eventName = eventName;
@@ -58,12 +63,28 @@ function defineElement(tagName, ElementClass) {
58
63
  }
59
64
  }
60
65
 
66
+ class BuyerIntentSessionElement extends BaseHTMLElement {
67
+ connectedCallback() {
68
+ this.style.display = 'contents';
69
+ }
70
+ }
71
+
72
+ class BuyerIntentSubjectElement extends BaseHTMLElement {
73
+ connectedCallback() {
74
+ this.style.display = 'none';
75
+ }
76
+ }
77
+
61
78
  class BuyerIntentViewElement extends BaseHTMLElement {
62
79
  connectedCallback() {
63
80
  ensureSDK();
64
81
  mergeIntentData(this, readIntentData(this));
65
82
  this.dataset.trigger = 'view';
66
- this.dispatchEvent(new CustomEvent('buyerintent:view', { bubbles: true }));
83
+ queueMicrotask(() => {
84
+ this.dispatchEvent(
85
+ new CustomEvent('buyerintent:view', { bubbles: true })
86
+ );
87
+ });
67
88
  }
68
89
  }
69
90
 
@@ -88,10 +109,14 @@ class BuyerIntentClickElement extends BaseHTMLElement {
88
109
  }
89
110
  }
90
111
 
112
+ defineElement('buyer-intent-session', BuyerIntentSessionElement);
113
+ defineElement('buyer-intent-subject', BuyerIntentSubjectElement);
91
114
  defineElement('buyer-intent-view', BuyerIntentViewElement);
92
115
  defineElement('buyer-intent-click', BuyerIntentClickElement);
93
116
 
94
117
  export {
118
+ BuyerIntentSessionElement,
119
+ BuyerIntentSubjectElement,
95
120
  BuyerIntentViewElement,
96
121
  BuyerIntentClickElement,
97
122
  mergeIntentData,
@@ -0,0 +1,6 @@
1
+ export {
2
+ BuyerIntentSessionElement,
3
+ BuyerIntentSubjectElement,
4
+ BuyerIntentViewElement,
5
+ BuyerIntentClickElement,
6
+ } from './base.js';
@@ -1,4 +1,6 @@
1
1
  export {
2
+ BuyerIntentSessionElement,
3
+ BuyerIntentSubjectElement,
2
4
  BuyerIntentViewElement,
3
5
  BuyerIntentClickElement,
4
6
  } from './elements/index.js';
@@ -1,7 +1,14 @@
1
1
  import { createBuyerIntentCore } from './core.js';
2
2
  import { sendWithBeacon } from './transport.js';
3
3
  import { createIdentityManager } from './identity.js';
4
- import { readTags, readElementConfig, resolveEventName } from './dom.js';
4
+ import {
5
+ readTags,
6
+ readElementConfig,
7
+ resolveEventName,
8
+ readSessionConfig,
9
+ collectSubjectIds,
10
+ readViewContext,
11
+ } from './dom.js';
5
12
 
6
13
  const DEFAULT_ACTIVITY_ENDPOINT = '/activity/events';
7
14
  const DEFAULT_VIEW_NAME = '$view';
@@ -79,23 +86,12 @@ export function createBuyerIntentSDK() {
79
86
  return entries.length > 0;
80
87
  }
81
88
 
82
- function trackPageview(element) {
89
+ function sendPayload(element, defaultEventName) {
83
90
  if (typeof window === 'undefined') {
84
91
  return false;
85
92
  }
86
93
 
87
- const pageKey = `${window.location.pathname}${window.location.search}`;
88
- if (pageKey === lastPageKey) {
89
- return false;
90
- }
91
-
92
- lastPageKey = pageKey;
93
- const hasTags = applyDomTags(element);
94
- if (!hasTags) {
95
- return false;
96
- }
97
-
98
- const eventName = resolveEventName(element, viewName);
94
+ const eventName = resolveEventName(element, defaultEventName);
99
95
  const payload = core.buildPayload({
100
96
  name: eventName,
101
97
  url: window.location.href,
@@ -110,25 +106,45 @@ export function createBuyerIntentSDK() {
110
106
  return sent;
111
107
  }
112
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
+
113
142
  function trackClick(element) {
114
143
  if (typeof window === 'undefined' || !element) {
115
144
  return false;
116
145
  }
117
146
 
118
- applyDomTags(element);
119
- const eventName = resolveEventName(element, DEFAULT_CLICK_NAME);
120
- const payload = core.buildPayload({
121
- name: eventName,
122
- url: window.location.href,
123
- origin: origin || window.location.hostname,
124
- distinctId: identity.ensure(),
125
- userType,
126
- visit: buildVisitContext(),
127
- });
128
-
129
- const sent = sendWithBeacon(activityEndpoint, payload);
130
- core.resetPageState();
131
- return sent;
147
+ return sendPayload(element, DEFAULT_CLICK_NAME);
132
148
  }
133
149
 
134
150
  function init(options = {}) {
@@ -196,13 +212,58 @@ export function createBuyerIntentSDK() {
196
212
  return () => nextRouter.events.off('routeChangeComplete', handler);
197
213
  }
198
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
+
199
239
  function handleView(element) {
200
240
  if (!element || firedViewElements.has(element)) {
201
241
  return false;
202
242
  }
203
243
 
204
244
  firedViewElements.add(element);
205
- return trackPageview(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);
206
267
  }
207
268
 
208
269
  function handleClick(element) {
@@ -210,7 +271,20 @@ export function createBuyerIntentSDK() {
210
271
  return false;
211
272
  }
212
273
 
213
- applyElementConfig(element);
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
+
214
288
  return trackClick(element);
215
289
  }
216
290
 
@@ -1,3 +1,5 @@
1
+ export class BuyerIntentSessionElement extends HTMLElement {}
2
+ export class BuyerIntentSubjectElement extends HTMLElement {}
1
3
  export class BuyerIntentViewElement extends HTMLElement {}
2
4
  export class BuyerIntentClickElement extends HTMLElement {}
3
5
 
@@ -11,10 +13,18 @@ export type ViewTag =
11
13
 
12
14
  export type ClickEventName = '/ad/clicked' | '/leads/create';
13
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
+
14
24
  interface BuyerIntentBaseAttrs {
15
25
  'data-origin'?: string;
16
26
  'data-activity-endpoint'?: string;
17
- 'data-user-type'?: string;
27
+ 'data-user-type'?: UserType;
18
28
  'data-distinct-id'?: string;
19
29
  'data-buyer-intent'?: string;
20
30
  'product-id'?: string;
@@ -22,17 +32,36 @@ interface BuyerIntentBaseAttrs {
22
32
  'category-id'?: string;
23
33
  }
24
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
+
25
49
  export interface BuyerIntentViewAttrs extends BuyerIntentBaseAttrs {
26
- tag: ViewTag;
50
+ tag?: ViewTag | string;
51
+ 'source-location'?: string;
27
52
  }
28
53
 
29
54
  export interface BuyerIntentClickAttrs extends BuyerIntentBaseAttrs {
30
- 'event-name': ClickEventName;
55
+ 'event-name'?: ClickEventName | string;
31
56
  }
32
57
 
33
58
  declare global {
34
59
  namespace JSX {
35
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>;
36
65
  'buyer-intent-view': BuyerIntentViewAttrs &
37
66
  React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
38
67
  'buyer-intent-click': BuyerIntentClickAttrs &
@@ -46,7 +75,7 @@ export type BuyerIntentInitOptions = {
46
75
  origin?: string;
47
76
  viewName?: string;
48
77
  distinctId?: string;
49
- userType?: string;
78
+ userType?: UserType;
50
79
  };
51
80
 
52
81
  export type BuyerIntentTagOptions = {
@@ -60,7 +89,7 @@ export type BuyerIntentTagOptions = {
60
89
  export type BuyerIntentBaseOptions = {
61
90
  origin?: string;
62
91
  distinctId?: string;
63
- userType?: string;
92
+ userType?: UserType;
64
93
  };
65
94
 
66
95
  export type BuyerIntentVisitProperties = {
@@ -1,6 +1,8 @@
1
1
  export {
2
2
  buyerIntent,
3
3
  createBuyerIntentSDK,
4
+ BuyerIntentSessionElement,
5
+ BuyerIntentSubjectElement,
4
6
  BuyerIntentViewElement,
5
7
  BuyerIntentClickElement,
6
8
  } from './browser/index.js';
@@ -1,15 +1,31 @@
1
1
  import type * as React from 'react';
2
+ import type { UserType } from '../index';
2
3
 
3
4
  type CommonProps = {
4
5
  sourceLocation?: string;
5
6
  context?: Record<string, unknown>;
6
7
  origin?: string;
7
8
  activityEndpoint?: string;
8
- userType?: string;
9
+ userType?: UserType;
9
10
  distinctId?: string;
10
11
  children?: React.ReactNode;
11
12
  };
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
+
13
29
  export type ViewTrackerProps = CommonProps & {
14
30
  tag?: string;
15
31
  productId?: number;
@@ -29,6 +45,8 @@ type CategoryViewProps = CommonProps & { categoryId: number };
29
45
  type ProductIdsViewProps = CommonProps & { productIds: number[] };
30
46
  type ProductIdClickProps = CommonProps & { productId: number };
31
47
 
48
+ export const SessionProvider: React.FC<SessionProviderProps>;
49
+ export const SubjectTracker: React.FC<SubjectTrackerProps>;
32
50
  export const ViewTracker: React.FC<ViewTrackerProps>;
33
51
  export const ClickTracker: React.FC<ClickTrackerProps>;
34
52
 
@@ -42,6 +60,10 @@ export const AdClick: React.FC<ProductIdClickProps>;
42
60
  export const LeadCreateClick: React.FC<ProductIdClickProps>;
43
61
 
44
62
  export const BuyerIntent: {
63
+ Session: React.FC<SessionProviderProps>;
64
+ Subject: React.FC<SubjectTrackerProps>;
65
+ View: React.FC<ViewTrackerProps>;
66
+ Click: React.FC<ClickTrackerProps>;
45
67
  ViewTracker: React.FC<ViewTrackerProps>;
46
68
  ClickTracker: React.FC<ClickTrackerProps>;
47
69
  ProfileView: React.FC<ProductIdViewProps>;
@@ -1,5 +1,7 @@
1
1
  import '../browser/elements/index.js';
2
2
  import {
3
+ SessionProvider,
4
+ SubjectTracker,
3
5
  ViewTracker,
4
6
  ClickTracker,
5
7
  ProfileView,
@@ -13,6 +15,8 @@ import {
13
15
  } from '../browser/components/index.js';
14
16
 
15
17
  export {
18
+ SessionProvider,
19
+ SubjectTracker,
16
20
  ViewTracker,
17
21
  ClickTracker,
18
22
  ProfileView,
@@ -26,6 +30,10 @@ export {
26
30
  };
27
31
 
28
32
  export const BuyerIntent = {
33
+ Session: SessionProvider,
34
+ Subject: SubjectTracker,
35
+ View: ViewTracker,
36
+ Click: ClickTracker,
29
37
  ViewTracker,
30
38
  ClickTracker,
31
39
  ProfileView,
@@ -1,40 +0,0 @@
1
- import React from 'react';
2
-
3
- function buildAttrs({
4
- productId,
5
- productIds,
6
- categoryId,
7
- tag,
8
- eventName,
9
- sourceLocation,
10
- context,
11
- origin,
12
- activityEndpoint,
13
- userType,
14
- distinctId,
15
- }) {
16
- const payload = { sourceLocation, context };
17
- const attrs = {
18
- 'data-buyer-intent': JSON.stringify(payload),
19
- 'data-origin': origin,
20
- 'data-activity-endpoint': activityEndpoint,
21
- 'data-user-type': userType,
22
- 'data-distinct-id': distinctId,
23
- };
24
-
25
- if (tag) attrs.tag = tag;
26
- if (eventName) attrs['event-name'] = eventName;
27
- if (productId != null) attrs['product-id'] = String(productId);
28
- if (productIds) attrs['product-ids'] = JSON.stringify(productIds);
29
- if (categoryId != null) attrs['category-id'] = String(categoryId);
30
-
31
- return attrs;
32
- }
33
-
34
- export function ViewTracker({ children, ...rest }) {
35
- return React.createElement('buyer-intent-view', buildAttrs(rest), children);
36
- }
37
-
38
- export function ClickTracker({ children, ...rest }) {
39
- return React.createElement('buyer-intent-click', buildAttrs(rest), children);
40
- }