@searchspring/snap-controller 0.73.7 → 0.74.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.
@@ -16,6 +16,9 @@ const defaultConfig = {
16
16
  selector: '',
17
17
  action: '',
18
18
  globals: {},
19
+ beacon: {
20
+ enabled: true,
21
+ },
19
22
  settings: {
20
23
  integratedSpellCorrection: false,
21
24
  initializeFromUrl: true,
@@ -43,6 +46,10 @@ export class AutocompleteController extends AbstractController {
43
46
  this.track = {
44
47
  banner: {
45
48
  impression: (_banner) => {
49
+ if (!_banner) {
50
+ this.log.warn('No banner provided to track.banner.impression');
51
+ return;
52
+ }
46
53
  const { responseId, uid } = _banner;
47
54
  if (this.events[responseId]?.banner?.[uid]?.impression) {
48
55
  return;
@@ -54,11 +61,15 @@ export class AutocompleteController extends AbstractController {
54
61
  results: [],
55
62
  };
56
63
  this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
57
- this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
64
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
58
65
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
59
66
  this.events[responseId].banner[uid].impression = true;
60
67
  },
61
68
  click: (e, banner) => {
69
+ if (!banner) {
70
+ this.log.warn('No banner provided to track.banner.click');
71
+ return;
72
+ }
62
73
  const { responseId, uid } = banner;
63
74
  if (isClickWithinBannerLink(e)) {
64
75
  if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
@@ -73,13 +84,17 @@ export class AutocompleteController extends AbstractController {
73
84
  }
74
85
  },
75
86
  clickThrough: (e, { uid, responseId }) => {
87
+ if (!uid) {
88
+ this.log.warn('No banner uid provided to track.banner.clickThrough');
89
+ return;
90
+ }
76
91
  const banner = { uid };
77
92
  const data = {
78
93
  responseId,
79
94
  banners: [banner],
80
95
  };
81
96
  this.eventManager.fire('track.banner.clickThrough', { controller: this, event: e, product: { uid }, trackEvent: data });
82
- this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
97
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
83
98
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
84
99
  this.events[responseId].banner[uid].clickThrough = true;
85
100
  setTimeout(() => {
@@ -89,21 +104,34 @@ export class AutocompleteController extends AbstractController {
89
104
  },
90
105
  product: {
91
106
  clickThrough: (e, result) => {
107
+ if (!result) {
108
+ this.log.warn('No result provided to track.product.clickThrough');
109
+ return;
110
+ }
92
111
  const responseId = result.responseId;
112
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
93
113
  const item = {
94
- type: result.type,
95
- uid: result.id,
96
- parentId: result.id,
97
- sku: result.mappings.core?.sku,
114
+ type,
115
+ uid: result.id ? '' + result.id : '',
116
+ ...(type === 'product'
117
+ ? {
118
+ parentId: result.id ? '' + result.id : '',
119
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
120
+ }
121
+ : {}),
98
122
  };
99
123
  const data = {
100
124
  responseId,
101
125
  results: [item],
102
126
  };
103
127
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
104
- this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
128
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
105
129
  },
106
130
  click: (e, result) => {
131
+ if (!result) {
132
+ this.log.warn('No result provided to track.product.click');
133
+ return;
134
+ }
107
135
  const responseId = result.responseId;
108
136
  if (result.type === 'banner' && isClickWithinBannerLink(e)) {
109
137
  if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
@@ -129,15 +157,24 @@ export class AutocompleteController extends AbstractController {
129
157
  }
130
158
  },
131
159
  impression: (result) => {
160
+ if (!result) {
161
+ this.log.warn('No result provided to track.product.impression');
162
+ return;
163
+ }
132
164
  const responseId = result.responseId;
133
165
  if (this.events?.[responseId]?.product[result.id]?.impression) {
134
166
  return;
135
167
  }
168
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
136
169
  const item = {
137
- type: result.type,
138
- uid: result.id,
139
- parentId: result.id,
140
- sku: result.mappings.core?.sku,
170
+ type,
171
+ uid: result.id ? '' + result.id : '',
172
+ ...(type === 'product'
173
+ ? {
174
+ parentId: result.id ? '' + result.id : '',
175
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
176
+ }
177
+ : {}),
141
178
  };
142
179
  const data = {
143
180
  responseId,
@@ -145,11 +182,15 @@ export class AutocompleteController extends AbstractController {
145
182
  banners: [],
146
183
  };
147
184
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
148
- this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
185
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
149
186
  this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
150
187
  this.events[responseId].product[result.id].impression = true;
151
188
  },
152
189
  addToCart: (result) => {
190
+ if (!result) {
191
+ this.log.warn('No result provided to track.product.addToCart');
192
+ return;
193
+ }
153
194
  const responseId = result.responseId;
154
195
  const product = {
155
196
  parentId: result.id,
@@ -163,16 +204,20 @@ export class AutocompleteController extends AbstractController {
163
204
  results: [product],
164
205
  };
165
206
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
166
- this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
207
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
167
208
  },
168
209
  },
169
210
  redirect: ({ redirectURL, responseId }) => {
211
+ if (!redirectURL) {
212
+ this.log.warn('No redirectURL provided to track.redirect');
213
+ return;
214
+ }
170
215
  const data = {
171
216
  responseId,
172
217
  redirect: redirectURL,
173
218
  };
174
219
  this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
175
- this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
220
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
176
221
  },
177
222
  };
178
223
  this.handlers = {
@@ -425,7 +470,8 @@ export class AutocompleteController extends AbstractController {
425
470
  this.log.profile(searchProfile);
426
471
  const responseId = response.tracking.responseId;
427
472
  this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
428
- if (response.search?.query === this.lastSearchQuery) {
473
+ const previousResponseId = this.store.results[0]?.responseId;
474
+ if (previousResponseId && previousResponseId === responseId) {
429
475
  const impressedResultIds = Object.keys(this.events[responseId].product || {}).filter((resultId) => this.events[responseId].product?.[resultId]?.impression);
430
476
  this.events[responseId] = {
431
477
  product: impressedResultIds.reduce((acc, resultId) => {
@@ -437,7 +483,6 @@ export class AutocompleteController extends AbstractController {
437
483
  }
438
484
  else {
439
485
  this.events[responseId] = { product: {}, banner: {} };
440
- this.lastSearchQuery = response.search?.query;
441
486
  }
442
487
  const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
443
488
  try {
@@ -462,6 +507,8 @@ export class AutocompleteController extends AbstractController {
462
507
  this.log.profile(afterSearchProfile);
463
508
  // update the store
464
509
  this.store.update(response);
510
+ const data = { responseId };
511
+ this.config.beacon?.enabled && this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
465
512
  const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
466
513
  try {
467
514
  await this.eventManager.fire('afterStore', {
@@ -530,7 +577,11 @@ export class AutocompleteController extends AbstractController {
530
577
  }
531
578
  };
532
579
  this.addToCart = async (_products) => {
533
- const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
580
+ const products = typeof _products?.slice == 'function' ? _products.slice() : [_products];
581
+ if (!_products || products.length === 0) {
582
+ this.log.warn('No products provided to autocomplete controller.addToCart');
583
+ return;
584
+ }
534
585
  products.forEach((product) => {
535
586
  this.track.product.addToCart(product);
536
587
  });
@@ -553,15 +604,6 @@ export class AutocompleteController extends AbstractController {
553
604
  type: 'session',
554
605
  key: `ss-controller-${this.config.id}`,
555
606
  });
556
- this.eventManager.on('afterStore', async (search, next) => {
557
- await next();
558
- const controller = search.controller;
559
- const responseId = search.response.tracking.responseId;
560
- if (controller.store.loaded && !controller.store.error) {
561
- const data = { responseId };
562
- this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
563
- }
564
- });
565
607
  // add 'afterSearch' middleware
566
608
  this.eventManager.on('afterSearch', async (ac, next) => {
567
609
  await next();
@@ -1 +1 @@
1
- {"version":3,"file":"FinderController.d.ts","sourceRoot":"","sources":["../../../src/Finder/FinderController.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAwB,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAiBnH,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;gBAGtC,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IA+B3B,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CA6BhC;IAED,IAAI,QAAa,QAAQ,IAAI,CAAC,CAkB5B;IAEF,KAAK,QAAO,IAAI,CAId;IAEF,MAAM,QAAa,QAAQ,IAAI,CAAC,CAoI9B;CACF"}
1
+ {"version":3,"file":"FinderController.d.ts","sourceRoot":"","sources":["../../../src/Finder/FinderController.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAwB,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAoBnH,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;gBAGtC,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IA+B3B,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CA6BhC;IAED,IAAI,QAAa,QAAQ,IAAI,CAAC,CAkB5B;IAEF,KAAK,QAAO,IAAI,CAId;IAEF,MAAM,QAAa,QAAQ,IAAI,CAAC,CAoI9B;CACF"}
@@ -5,6 +5,9 @@ import { getSearchParams } from '../utils/getParams';
5
5
  import { ControllerTypes } from '../types';
6
6
  const defaultConfig = {
7
7
  id: 'finder',
8
+ beacon: {
9
+ enabled: true,
10
+ },
8
11
  globals: {
9
12
  pagination: {
10
13
  pageSize: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"RecommendationController.d.ts","sourceRoot":"","sources":["../../../src/Recommendation/RecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAW3C,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,gBAAgB,EAAiB,MAAM,UAAU,CAAC;AAIpH,KAAK,0BAA0B,GAAG;IACjC,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;KACrC,CAAC;CACF,CAAC;AAUF,qBAAa,wBAAyB,SAAQ,kBAAkB;IACxD,IAAI,kBAAkC;IACrC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,8BAA8B,CAAC;IAE/C,OAAO,CAAC,MAAM,CAUP;gBAGN,MAAM,EAAE,8BAA8B,EACtC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAkD3B,KAAK,EAAE,0BAA0B,CAqF/B;IAEF,IAAI,MAAM,IAAI,qBAAqB,CA4BlC;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CA6H9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAQ/D;CACF"}
1
+ {"version":3,"file":"RecommendationController.d.ts","sourceRoot":"","sources":["../../../src/Recommendation/RecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAW3C,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,KAAK,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGrG,KAAK,0BAA0B,GAAG;IACjC,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;KACrC,CAAC;CACF,CAAC;AAaF,qBAAa,wBAAyB,SAAQ,kBAAkB;IACxD,IAAI,kBAAkC;IACrC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,8BAA8B,CAAC;IAE/C,OAAO,CAAC,MAAM,CAUP;gBAGN,MAAM,EAAE,8BAA8B,EACtC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAwC3B,KAAK,EAAE,0BAA0B,CA+G/B;IAEF,IAAI,MAAM,IAAI,qBAAqB,CA4BlC;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CAgI9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAY/D;CACF"}
@@ -5,6 +5,9 @@ import { ControllerTypes } from '../types';
5
5
  import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
6
6
  const defaultConfig = {
7
7
  id: 'recommend',
8
+ beacon: {
9
+ enabled: true,
10
+ },
8
11
  tag: '',
9
12
  batched: true,
10
13
  realtime: false,
@@ -18,14 +21,23 @@ export class RecommendationController extends AbstractController {
18
21
  this.track = {
19
22
  product: {
20
23
  clickThrough: (e, result) => {
24
+ if (!result) {
25
+ this.log.warn('No result provided to track.product.clickThrough');
26
+ return;
27
+ }
21
28
  const responseId = result.responseId;
22
29
  if (this.events[responseId]?.product[result.id]?.productClickThrough)
23
30
  return;
31
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
24
32
  const beaconResult = {
25
- type: result.type,
26
- uid: result.id,
27
- parentId: result.id,
28
- sku: result.mappings.core?.sku,
33
+ type,
34
+ uid: result.id ? '' + result.id : '',
35
+ ...(type === 'product'
36
+ ? {
37
+ parentId: result.id ? '' + result.id : '',
38
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
39
+ }
40
+ : {}),
29
41
  };
30
42
  const data = {
31
43
  tag: this.store.profile.tag,
@@ -33,11 +45,15 @@ export class RecommendationController extends AbstractController {
33
45
  results: [beaconResult],
34
46
  };
35
47
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
36
- this.tracker.events.recommendations.clickThrough({ data, siteId: this.config.globals?.siteId });
48
+ this.config.beacon?.enabled && this.tracker.events.recommendations.clickThrough({ data, siteId: this.config.globals?.siteId });
37
49
  this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
38
50
  this.events[responseId].product[result.id].productClickThrough = true;
39
51
  },
40
52
  click: (e, result) => {
53
+ if (!result) {
54
+ this.log.warn('No result provided to track.product.click');
55
+ return;
56
+ }
41
57
  const responseId = result.responseId;
42
58
  if (result.type === 'banner') {
43
59
  if (this.events[responseId]?.product[result.id]?.inlineBannerClickThrough) {
@@ -63,15 +79,24 @@ export class RecommendationController extends AbstractController {
63
79
  }
64
80
  },
65
81
  impression: (result) => {
82
+ if (!result) {
83
+ this.log.warn('No result provided to track.product.impression');
84
+ return;
85
+ }
66
86
  const responseId = result.responseId;
67
87
  if (this.events[responseId]?.product[result.id]?.impression) {
68
88
  return;
69
89
  }
90
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
70
91
  const item = {
71
- type: result.type,
72
- uid: result.id,
73
- parentId: result.id,
74
- sku: result.mappings.core?.sku,
92
+ type,
93
+ uid: result.id ? '' + result.id : '',
94
+ ...(type === 'product'
95
+ ? {
96
+ parentId: result.id ? '' + result.id : '',
97
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
98
+ }
99
+ : {}),
75
100
  };
76
101
  const data = {
77
102
  tag: this.store.profile.tag,
@@ -80,11 +105,15 @@ export class RecommendationController extends AbstractController {
80
105
  banners: [],
81
106
  };
82
107
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
83
- this.tracker.events.recommendations.impression({ data, siteId: this.config.globals?.siteId });
108
+ this.config.beacon?.enabled && this.tracker.events.recommendations.impression({ data, siteId: this.config.globals?.siteId });
84
109
  this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
85
110
  this.events[responseId].product[result.id].impression = true;
86
111
  },
87
112
  addToCart: (result) => {
113
+ if (!result) {
114
+ this.log.warn('No result provided to track.product.addToCart');
115
+ return;
116
+ }
88
117
  const responseId = result.responseId;
89
118
  const product = {
90
119
  parentId: result.id,
@@ -99,7 +128,7 @@ export class RecommendationController extends AbstractController {
99
128
  results: [product],
100
129
  };
101
130
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
102
- this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
131
+ this.config.beacon?.enabled && this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
103
132
  },
104
133
  },
105
134
  };
@@ -155,6 +184,8 @@ export class RecommendationController extends AbstractController {
155
184
  this.log.profile(afterSearchProfile);
156
185
  // update the store
157
186
  this.store.update(response);
187
+ const data = { responseId, tag: this.store.profile.tag };
188
+ this.config.beacon?.enabled && this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
158
189
  const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
159
190
  try {
160
191
  await this.eventManager.fire('afterStore', {
@@ -223,7 +254,11 @@ export class RecommendationController extends AbstractController {
223
254
  }
224
255
  };
225
256
  this.addToCart = async (_products) => {
226
- const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
257
+ const products = typeof _products?.slice == 'function' ? _products.slice() : [_products];
258
+ if (!_products || products.length === 0) {
259
+ this.log.warn('No products provided to recommendation controller.addToCart');
260
+ return;
261
+ }
227
262
  products.forEach((product) => {
228
263
  this.track.product.addToCart(product);
229
264
  });
@@ -246,15 +281,6 @@ export class RecommendationController extends AbstractController {
246
281
  // deep merge config with defaults
247
282
  this.config = deepmerge(defaultConfig, this.config);
248
283
  this.store.setConfig(this.config);
249
- this.eventManager.on('afterStore', async (search, next) => {
250
- await next();
251
- const controller = search.controller;
252
- const responseId = search.response.responseId;
253
- if (controller.store.loaded && !controller.store.error) {
254
- const data = { responseId, tag: controller.store.profile.tag };
255
- this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
256
- }
257
- });
258
284
  // add 'afterStore' middleware
259
285
  // this.eventManager.on('afterStore', async (recommend: AfterStoreObj, next: Next): Promise<void | boolean> => {
260
286
  // await next();
@@ -1 +1 @@
1
- {"version":3,"file":"SearchController.d.ts","sourceRoot":"","sources":["../../../src/Search/SearchController.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAa,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAuB,MAAM,UAAU,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAc,MAAM,+BAA+B,CAAC;AAC9F,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,EACN,KAAK,kBAAkB,EAYvB,MAAM,2BAA2B,CAAC;AAsCnC,KAAK,kBAAkB,GAAG;IACzB,MAAM,EAAE;QACP,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QAChF,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QACvF,UAAU,EAAE,CAAC,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;KACtE,CAAC;IACF,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7F,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;IACvC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,IAAI,CAEV;IACF,OAAO,CAAC,MAAM,CAgBP;gBAGN,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAoP3B,KAAK,EAAE,kBAAkB,CAqKvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CA0N9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAQ/D;CACF;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAiBxF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,GAAG,SAAS,CAgCvG"}
1
+ {"version":3,"file":"SearchController.d.ts","sourceRoot":"","sources":["../../../src/Search/SearchController.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,YAAY,EAAa,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEpG,OAAO,EAAE,eAAe,EAAuB,MAAM,UAAU,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAc,MAAM,+BAA+B,CAAC;AAC9F,OAAO,KAAK,EACX,sBAAsB,EAGtB,kBAAkB,EAClB,gBAAgB,EAIhB,MAAM,UAAU,CAAC;AAElB,OAAO,EACN,KAAK,kBAAkB,EAYvB,MAAM,2BAA2B,CAAC;AAyCnC,KAAK,kBAAkB,GAAG;IACzB,MAAM,EAAE;QACP,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QAChF,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;QACvF,UAAU,EAAE,CAAC,mBAAmB,EAAE,0BAA0B,KAAK,IAAI,CAAC;KACtE,CAAC;IACF,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QAC/C,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7F,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,kBAAkB;IAChD,IAAI,kBAA0B;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,sBAAsB,CAAC;IACvC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,eAAe,CAAwC;IAC/D,OAAO,CAAC,IAAI,CAEV;IACF,OAAO,CAAC,MAAM,CAgBP;gBAGN,MAAM,EAAE,sBAAsB,EAC9B,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAC1F,OAAO,CAAC,EAAE,gBAAgB;IAiP3B,KAAK,EAAE,kBAAkB,CA+MvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CA6N9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAY/D;CACF;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAiBxF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,GAAG,SAAS,CAuCvG"}
@@ -11,6 +11,9 @@ const BACKGROUND_FILTERS_VALUE_FLAGS = [1, 0, '1', '0', 'true', 'false', true, f
11
11
  const defaultConfig = {
12
12
  id: 'search',
13
13
  globals: {},
14
+ beacon: {
15
+ enabled: true,
16
+ },
14
17
  settings: {
15
18
  redirects: {
16
19
  merchandising: true,
@@ -36,6 +39,10 @@ export class SearchController extends AbstractController {
36
39
  this.track = {
37
40
  banner: {
38
41
  impression: ({ uid, responseId }) => {
42
+ if (!uid) {
43
+ this.log.warn('No banner provided to track.banner.impression');
44
+ return;
45
+ }
39
46
  if (this.events[responseId]?.banner[uid]?.impression) {
40
47
  return;
41
48
  }
@@ -46,11 +53,15 @@ export class SearchController extends AbstractController {
46
53
  results: [],
47
54
  };
48
55
  this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
49
- this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
56
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
50
57
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
51
58
  this.events[responseId].banner[uid].impression = true;
52
59
  },
53
60
  click: (e, banner) => {
61
+ if (!banner) {
62
+ this.log.warn('No banner provided to track.banner.click');
63
+ return;
64
+ }
54
65
  const { responseId, uid } = banner;
55
66
  if (isClickWithinBannerLink(e)) {
56
67
  if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
@@ -65,13 +76,17 @@ export class SearchController extends AbstractController {
65
76
  }
66
77
  },
67
78
  clickThrough: (e, { uid, responseId }) => {
79
+ if (!uid) {
80
+ this.log.warn('No banner provided to track.banner.clickThrough');
81
+ return;
82
+ }
68
83
  const banner = { uid };
69
84
  const data = {
70
85
  responseId,
71
86
  banners: [banner],
72
87
  };
73
88
  this.eventManager.fire('track.banner.clickThrough', { controller: this, event: e, product: { uid }, trackEvent: data });
74
- this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
89
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
75
90
  this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
76
91
  this.events[responseId].banner[uid].clickThrough = true;
77
92
  setTimeout(() => {
@@ -81,6 +96,10 @@ export class SearchController extends AbstractController {
81
96
  },
82
97
  product: {
83
98
  clickThrough: (e, result) => {
99
+ if (!result) {
100
+ this.log.warn('No result provided to track.product.clickThrough');
101
+ return;
102
+ }
84
103
  const responseId = result.responseId;
85
104
  const target = e.target;
86
105
  const resultHref = result.display?.mappings.core?.url || result.mappings.core?.url || '';
@@ -108,20 +127,29 @@ export class SearchController extends AbstractController {
108
127
  }
109
128
  // store position data or empty object
110
129
  this.storage.set('scrollMap', scrollMap);
130
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
111
131
  const item = {
112
- type: result.type,
113
- uid: result.id,
114
- parentId: result.id,
115
- sku: result.mappings.core?.sku,
132
+ type,
133
+ uid: result.id ? '' + result.id : '',
134
+ ...(type === 'product'
135
+ ? {
136
+ parentId: result.id ? '' + result.id : '',
137
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
138
+ }
139
+ : {}),
116
140
  };
117
141
  const data = {
118
142
  responseId,
119
143
  results: [item],
120
144
  };
121
145
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
122
- this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
146
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
123
147
  },
124
148
  click: (e, result) => {
149
+ if (!result) {
150
+ this.log.warn('No result provided to track.product.click');
151
+ return;
152
+ }
125
153
  const responseId = result.responseId;
126
154
  if (result.type === 'banner' && isClickWithinBannerLink(e)) {
127
155
  if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
@@ -147,15 +175,24 @@ export class SearchController extends AbstractController {
147
175
  }
148
176
  },
149
177
  impression: (result) => {
178
+ if (!result) {
179
+ this.log.warn('No result provided to track.product.impression');
180
+ return;
181
+ }
150
182
  const responseId = result.responseId;
151
183
  if (this.events[responseId]?.product[result.id]?.impression) {
152
184
  return;
153
185
  }
186
+ const type = (['product', 'banner'].includes(result.type) ? result.type : 'product');
154
187
  const item = {
155
- type: result.type,
156
- uid: result.id,
157
- parentId: result.id,
158
- sku: result.mappings.core?.sku,
188
+ type,
189
+ uid: result.id ? '' + result.id : '',
190
+ ...(type === 'product'
191
+ ? {
192
+ parentId: result.id ? '' + result.id : '',
193
+ sku: result.mappings.core?.sku ? '' + result.mappings.core?.sku : undefined,
194
+ }
195
+ : {}),
159
196
  };
160
197
  const data = {
161
198
  responseId,
@@ -163,11 +200,15 @@ export class SearchController extends AbstractController {
163
200
  banners: [],
164
201
  };
165
202
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
166
- this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
203
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
167
204
  this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
168
205
  this.events[responseId].product[result.id].impression = true;
169
206
  },
170
207
  addToCart: (result) => {
208
+ if (!result) {
209
+ this.log.warn('No result provided to track.product.addToCart');
210
+ return;
211
+ }
171
212
  const responseId = result.responseId;
172
213
  const product = {
173
214
  parentId: result.id,
@@ -181,16 +222,20 @@ export class SearchController extends AbstractController {
181
222
  results: [product],
182
223
  };
183
224
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
184
- this.tracker.events[this.page.type].addToCart({ data, siteId: this.config.globals?.siteId });
225
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].addToCart({ data, siteId: this.config.globals?.siteId });
185
226
  },
186
227
  },
187
228
  redirect: ({ redirectURL, responseId }) => {
229
+ if (!redirectURL) {
230
+ this.log.warn('No redirectURL provided to track.redirect');
231
+ return;
232
+ }
188
233
  const data = {
189
234
  responseId,
190
235
  redirect: redirectURL,
191
236
  };
192
237
  this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
193
- this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
238
+ this.config.beacon?.enabled && this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
194
239
  },
195
240
  };
196
241
  this.search = async () => {
@@ -324,6 +369,8 @@ export class SearchController extends AbstractController {
324
369
  this.previousResults = JSON.parse(JSON.stringify(response.results));
325
370
  // update the store
326
371
  this.store.update(response);
372
+ const data = { responseId: response.tracking.responseId };
373
+ this.config.beacon?.enabled && this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
327
374
  const afterStoreProfile = this.profiler.create({ type: 'event', name: 'afterStore', context: params }).start();
328
375
  try {
329
376
  await this.eventManager.fire('afterStore', {
@@ -392,7 +439,11 @@ export class SearchController extends AbstractController {
392
439
  }
393
440
  };
394
441
  this.addToCart = async (_products) => {
395
- const products = typeof _products.slice == 'function' ? _products.slice() : [_products];
442
+ const products = typeof _products?.slice == 'function' ? _products.slice() : [_products];
443
+ if (!_products || products.length === 0) {
444
+ this.log.warn('No products provided to search controller.addToCart');
445
+ return;
446
+ }
396
447
  products.forEach((product) => {
397
448
  this.track.product.addToCart(product);
398
449
  });
@@ -514,10 +565,7 @@ export class SearchController extends AbstractController {
514
565
  this.eventManager.on('afterStore', async (search, next) => {
515
566
  await next();
516
567
  const controller = search.controller;
517
- const responseId = search.response.tracking.responseId;
518
568
  if (controller.store.loaded && !controller.store.error) {
519
- const data = { responseId };
520
- this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
521
569
  const config = search.controller.config;
522
570
  const nonBackgroundFilters = search?.request?.filters?.filter((filter) => !filter.background);
523
571
  if (config?.settings?.redirects?.singleResult &&
@@ -666,8 +714,16 @@ export function generateHrefSelector(element, href, levels = 7) {
666
714
  let level = 0;
667
715
  let elem = element;
668
716
  while (elem && level <= levels) {
669
- // check within
670
- const innerHrefElem = elem.querySelector(`[href*="${href}"]`);
717
+ let innerHrefElem = null;
718
+ try {
719
+ innerHrefElem = elem.querySelector(`[href*="${href}"]`);
720
+ }
721
+ catch (e) {
722
+ try {
723
+ innerHrefElem = elem.querySelector(cssEscape(`[href*="${href}"]`));
724
+ }
725
+ catch { }
726
+ }
671
727
  if (innerHrefElem) {
672
728
  // innerHrefElem was found! now get selectors up to elem that contained it
673
729
  let selector = '';
@@ -1 +1 @@
1
- {"version":3,"file":"isClickWithinProductLink.d.ts","sourceRoot":"","sources":["../../../src/utils/isClickWithinProductLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE7D,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,eAAO,MAAM,wBAAwB,MAAO,UAAU,UAAU,OAAO,KAAG,OAgBzE,CAAC"}
1
+ {"version":3,"file":"isClickWithinProductLink.d.ts","sourceRoot":"","sources":["../../../src/utils/isClickWithinProductLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE7D,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,eAAO,MAAM,wBAAwB,MAAO,UAAU,UAAU,OAAO,KAAG,OAoBzE,CAAC"}