@searchspring/snap-controller 0.72.2 → 0.73.5

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.
Files changed (25) hide show
  1. package/dist/cjs/Autocomplete/AutocompleteController.d.ts +13 -13
  2. package/dist/cjs/Autocomplete/AutocompleteController.d.ts.map +1 -1
  3. package/dist/cjs/Autocomplete/AutocompleteController.js +151 -147
  4. package/dist/cjs/Recommendation/RecommendationController.d.ts +5 -13
  5. package/dist/cjs/Recommendation/RecommendationController.d.ts.map +1 -1
  6. package/dist/cjs/Recommendation/RecommendationController.js +81 -92
  7. package/dist/cjs/Search/SearchController.d.ts +12 -5
  8. package/dist/cjs/Search/SearchController.d.ts.map +1 -1
  9. package/dist/cjs/Search/SearchController.js +144 -168
  10. package/dist/cjs/utils/isClickWithinBannerLink.d.ts +2 -0
  11. package/dist/cjs/utils/isClickWithinBannerLink.d.ts.map +1 -0
  12. package/dist/cjs/utils/isClickWithinBannerLink.js +21 -0
  13. package/dist/esm/Autocomplete/AutocompleteController.d.ts +13 -13
  14. package/dist/esm/Autocomplete/AutocompleteController.d.ts.map +1 -1
  15. package/dist/esm/Autocomplete/AutocompleteController.js +131 -139
  16. package/dist/esm/Recommendation/RecommendationController.d.ts +5 -13
  17. package/dist/esm/Recommendation/RecommendationController.d.ts.map +1 -1
  18. package/dist/esm/Recommendation/RecommendationController.js +72 -80
  19. package/dist/esm/Search/SearchController.d.ts +12 -5
  20. package/dist/esm/Search/SearchController.d.ts.map +1 -1
  21. package/dist/esm/Search/SearchController.js +128 -165
  22. package/dist/esm/utils/isClickWithinBannerLink.d.ts +2 -0
  23. package/dist/esm/utils/isClickWithinBannerLink.d.ts.map +1 -0
  24. package/dist/esm/utils/isClickWithinBannerLink.js +17 -0
  25. package/package.json +10 -10
@@ -3,8 +3,8 @@ import { StorageStore, ErrorType } from '@searchspring/snap-store-mobx';
3
3
  import { AbstractController } from '../Abstract/AbstractController';
4
4
  import { getSearchParams } from '../utils/getParams';
5
5
  import { ControllerTypes } from '../types';
6
- import { ItemTypeEnum, } from '@searchspring/beacon';
7
6
  import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
7
+ import { isClickWithinBannerLink } from '../utils/isClickWithinBannerLink';
8
8
  const INPUT_ATTRIBUTE = 'ss-autocomplete-input';
9
9
  export const INPUT_DELAY = 200;
10
10
  const KEY_ENTER = 13;
@@ -39,61 +39,137 @@ export class AutocompleteController extends AbstractController {
39
39
  constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
40
40
  super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
41
41
  this.type = ControllerTypes.autocomplete;
42
- this.events = {
43
- product: {},
44
- };
42
+ this.events = {};
45
43
  this.track = {
46
- product: {
47
- clickThrough: (e, result) => {
48
- if (this.events.product[result.id]?.clickThrough) {
44
+ banner: {
45
+ impression: (banner) => {
46
+ const { responseId, uid } = banner;
47
+ if (this.events[responseId]?.banner?.[uid]?.impression) {
49
48
  return;
50
49
  }
51
- const data = getAutocompleteSchemaData({ params: this.params, store: this.store, results: [result] });
50
+ const data = {
51
+ responseId,
52
+ banners: [banner],
53
+ results: [],
54
+ };
55
+ this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
56
+ this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
57
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
58
+ this.events[responseId].banner[uid].impression = true;
59
+ },
60
+ click: (e, banner) => {
61
+ const { responseId, uid } = banner;
62
+ if (isClickWithinBannerLink(e)) {
63
+ if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
64
+ return;
65
+ }
66
+ this.track.banner.clickThrough(e, banner);
67
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
68
+ this.events[responseId].banner[uid].clickThrough = true;
69
+ setTimeout(() => {
70
+ this.events[responseId].banner[uid].clickThrough = false;
71
+ }, CLICK_DUPLICATION_TIMEOUT);
72
+ }
73
+ },
74
+ clickThrough: (e, { uid, responseId }) => {
75
+ const banner = { uid };
76
+ const data = {
77
+ responseId,
78
+ banners: [banner],
79
+ };
80
+ this.eventManager.fire('track.banner.clickThrough', { controller: this, event: e, product: { uid }, trackEvent: data });
81
+ this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
82
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
83
+ this.events[responseId].banner[uid].clickThrough = true;
84
+ setTimeout(() => {
85
+ this.events[responseId].banner[uid].clickThrough = false;
86
+ }, CLICK_DUPLICATION_TIMEOUT);
87
+ },
88
+ },
89
+ product: {
90
+ clickThrough: (e, result) => {
91
+ const responseId = result.responseId;
92
+ const item = {
93
+ type: result.type,
94
+ uid: result.id,
95
+ parentId: result.id,
96
+ sku: result.mappings.core?.sku,
97
+ };
98
+ const data = {
99
+ responseId,
100
+ results: [item],
101
+ };
52
102
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
53
103
  this.tracker.events.autocomplete.clickThrough({ data, siteId: this.config.globals?.siteId });
54
- this.events.product[result.id] = this.events.product[result.id] || {};
55
- this.events.product[result.id].clickThrough = true;
56
104
  },
57
105
  click: (e, result) => {
58
- if (this.events.product[result.id]?.click) {
59
- return;
106
+ const responseId = result.responseId;
107
+ if (result.type === 'banner' && isClickWithinBannerLink(e)) {
108
+ if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
109
+ return;
110
+ }
111
+ this.track.product.clickThrough(e, result);
112
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
113
+ this.events[responseId].product[result.id].inlineBannerClickThrough = true;
114
+ setTimeout(() => {
115
+ this.events[responseId].product[result.id].inlineBannerClickThrough = false;
116
+ }, CLICK_DUPLICATION_TIMEOUT);
60
117
  }
61
- if (result.type === 'banner') {
62
- return;
118
+ else if (isClickWithinProductLink(e, result)) {
119
+ if (this.events?.[responseId]?.product[result.id]?.productClickThrough) {
120
+ return;
121
+ }
122
+ this.track.product.clickThrough(e, result);
123
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
124
+ this.events[responseId].product[result.id].productClickThrough = true;
125
+ setTimeout(() => {
126
+ this.events[responseId].product[result.id].productClickThrough = false;
127
+ }, CLICK_DUPLICATION_TIMEOUT);
63
128
  }
64
- isClickWithinProductLink(e, result) && this.track.product.clickThrough(e, result);
65
- this.events.product[result.id] = this.events.product[result.id] || {};
66
- this.events.product[result.id].click = true;
67
- setTimeout(() => {
68
- this.events.product[result.id].click = false;
69
- }, CLICK_DUPLICATION_TIMEOUT);
70
- },
71
- render: (result) => {
72
- if (this.events.product[result.id]?.render)
73
- return;
74
- const data = getAutocompleteSchemaData({ params: this.params, store: this.store, results: result ? [result] : [] });
75
- this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
76
- this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
77
- this.events.product[result.id] = this.events.product[result.id] || {};
78
- this.events.product[result.id].render = true;
79
129
  },
80
130
  impression: (result) => {
81
- if (this.events.product[result.id]?.impression || !this.events.product[result.id]?.render)
131
+ const responseId = result.responseId;
132
+ if (this.events?.[responseId]?.product[result.id]?.impression) {
82
133
  return;
83
- const data = getAutocompleteSchemaData({ params: this.params, store: this.store, results: [result] });
134
+ }
135
+ const item = {
136
+ type: result.type,
137
+ uid: result.id,
138
+ parentId: result.id,
139
+ sku: result.mappings.core?.sku,
140
+ };
141
+ const data = {
142
+ responseId,
143
+ results: [item],
144
+ banners: [],
145
+ };
84
146
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
85
147
  this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
86
- this.events.product[result.id] = this.events.product[result.id] || {};
87
- this.events.product[result.id].impression = true;
148
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
149
+ this.events[responseId].product[result.id].impression = true;
88
150
  },
89
151
  addToCart: (result) => {
90
- const data = getAutocompleteAddtocartSchemaData({ params: this.params, store: this.store, results: [result] });
152
+ const responseId = result.responseId;
153
+ const product = {
154
+ parentId: result.id,
155
+ uid: result.id,
156
+ sku: result.mappings.core?.sku,
157
+ qty: result.quantity || 1,
158
+ price: Number(result.mappings.core?.price),
159
+ };
160
+ const data = {
161
+ responseId,
162
+ results: [product],
163
+ };
91
164
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
92
165
  this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
93
166
  },
94
167
  },
95
- redirect: (redirectURL) => {
96
- const data = getAutocompleteRedirectSchemaData({ redirectURL });
168
+ redirect: ({ redirectURL, responseId }) => {
169
+ const data = {
170
+ responseId,
171
+ redirect: redirectURL,
172
+ };
97
173
  this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
98
174
  this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
99
175
  },
@@ -230,6 +306,7 @@ export class AutocompleteController extends AbstractController {
230
306
  if (((!this.store.state.input && !value) || this.store.state.input == value) && this.store.loaded) {
231
307
  return;
232
308
  }
309
+ this.store.state.source = 'input';
233
310
  this.store.state.input = value;
234
311
  // remove merch redirect to prevent race condition
235
312
  this.store.merchandising.redirect = '';
@@ -345,17 +422,20 @@ export class AutocompleteController extends AbstractController {
345
422
  }
346
423
  searchProfile.stop();
347
424
  this.log.profile(searchProfile);
425
+ const responseId = response.tracking.responseId;
426
+ this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
348
427
  if (response.search?.query === this.lastSearchQuery) {
349
- const impressedResultIds = Object.keys(this.events.product).filter((resultId) => this.events.product[resultId]?.impression);
350
- this.events = {
428
+ const impressedResultIds = Object.keys(this.events[responseId].product || {}).filter((resultId) => this.events[responseId].product?.[resultId]?.impression);
429
+ this.events[responseId] = {
351
430
  product: impressedResultIds.reduce((acc, resultId) => {
352
431
  acc[resultId] = { impression: true };
353
432
  return acc;
354
433
  }, {}),
434
+ banner: this.events[responseId].banner,
355
435
  };
356
436
  }
357
437
  else {
358
- this.events = { product: {} };
438
+ this.events[responseId] = { product: {}, banner: {} };
359
439
  this.lastSearchQuery = response.search?.query;
360
440
  }
361
441
  const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
@@ -475,23 +555,12 @@ export class AutocompleteController extends AbstractController {
475
555
  this.eventManager.on('afterStore', async (search, next) => {
476
556
  await next();
477
557
  const controller = search.controller;
558
+ const responseId = search.response.tracking.responseId;
478
559
  if (controller.store.loaded && !controller.store.error) {
479
- const products = controller.store.results.filter((result) => result.type === 'product');
480
- if (products.length === 0 && !search.response._cached) {
481
- // handle no results
482
- const data = getAutocompleteSchemaData({ params: search.request, store: this.store });
483
- this.eventManager.fire('track.product.render', { controller: this, trackEvent: data });
560
+ if (!search.response._cached) {
561
+ const data = { responseId };
484
562
  this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
485
563
  }
486
- products.forEach((result) => {
487
- if (!search.response._cached) {
488
- this.track.product.render(result);
489
- }
490
- else {
491
- this.events.product[result.id] = this.events.product[result.id] || {};
492
- this.events.product[result.id].render = true;
493
- }
494
- });
495
564
  }
496
565
  });
497
566
  // add 'afterSearch' middleware
@@ -510,7 +579,7 @@ export class AutocompleteController extends AbstractController {
510
579
  const inputState = ac.controller.store.state.input;
511
580
  const redirectURL = ac.controller.store.merchandising?.redirect;
512
581
  if (this.config?.settings?.redirects?.merchandising && inputState && redirectURL) {
513
- this.track.redirect(redirectURL);
582
+ this.track.redirect({ redirectURL, responseId: ac.controller.store.merchandising?.responseId });
514
583
  window.location.href = redirectURL;
515
584
  return false;
516
585
  }
@@ -543,6 +612,14 @@ export class AutocompleteController extends AbstractController {
543
612
  if (pageLoadId) {
544
613
  params.tracking.pageLoadId = pageLoadId;
545
614
  }
615
+ if (this.store.state.input) {
616
+ params.search = params.search || {};
617
+ params.search.input = this.store.state.input;
618
+ }
619
+ if (this.store.state.source) {
620
+ params.search = params.search || {};
621
+ params.search.source = this.store.state.source;
622
+ }
546
623
  if (!this.config.globals?.personalization?.disabled) {
547
624
  const cartItems = this.tracker.cookies.cart.get();
548
625
  if (cartItems.length) {
@@ -732,88 +809,3 @@ function unbindFormParameters(form, fn) {
732
809
  }
733
810
  }
734
811
  }
735
- function getAutocompleteRedirectSchemaData({ redirectURL }) {
736
- return {
737
- redirect: redirectURL,
738
- };
739
- }
740
- function getAutocompleteAddtocartSchemaData({ params, store, results, }) {
741
- const base = getAutocompleteSchemaData({ params, store, results });
742
- return {
743
- ...base,
744
- results: results?.map((result) => {
745
- const core = result.mappings.core;
746
- return {
747
- uid: core.uid || '',
748
- sku: core.sku,
749
- price: Number(core.price),
750
- qty: result.quantity || 1,
751
- };
752
- }) || [],
753
- };
754
- }
755
- function getAutocompleteSchemaData({ params, store, results, }) {
756
- const filters = params.filters?.reduce((acc, filter) => {
757
- const key = filter.background ? 'bgfilter' : 'filter';
758
- acc[key] = acc[key] || [];
759
- const value = filter.type === 'range' &&
760
- !isNaN(filter.value?.low) &&
761
- !isNaN(filter.value?.high)
762
- ? [`low=${filter.value?.low}`, `high=${filter.value?.high}`]
763
- : [`${filter.value}`];
764
- const existing = acc[key].find((item) => item.field === filter.field);
765
- if (existing && !existing.value.includes(value[0])) {
766
- existing.value.push(...value);
767
- }
768
- else {
769
- acc[key].push({
770
- field: filter.field,
771
- value,
772
- });
773
- }
774
- return acc;
775
- }, {});
776
- return {
777
- q: store.search?.originalQuery?.string || store.search?.query?.string || '',
778
- rq: params.search?.subQuery ? params.search?.subQuery : undefined,
779
- correctedQuery: store.search?.originalQuery?.string ? store.search?.query?.string : undefined,
780
- matchType: store.search.matchType,
781
- ...filters,
782
- sort: params.sorts?.map((sort) => {
783
- return {
784
- field: sort.field,
785
- dir: sort.direction,
786
- };
787
- }),
788
- pagination: {
789
- totalResults: store.pagination.totalResults,
790
- page: store.pagination.page,
791
- resultsPerPage: store.pagination.pageSize,
792
- },
793
- merchandising: {
794
- personalized: store.merchandising.personalized,
795
- redirect: store.merchandising.redirect,
796
- triggeredCampaigns: (store.merchandising.campaigns?.length &&
797
- store.merchandising.campaigns?.map((campaign) => {
798
- const experiement = store.merchandising.experiments.find((experiment) => experiment.campaignId === campaign.id);
799
- return {
800
- id: campaign.id,
801
- experimentId: experiement?.experimentId,
802
- variationId: experiement?.variationId,
803
- };
804
- })) ||
805
- undefined,
806
- },
807
- banners: [],
808
- results: results?.map((result) => {
809
- const core = result.mappings.core;
810
- const position = result.position;
811
- return {
812
- type: ItemTypeEnum.Product,
813
- position,
814
- uid: core.uid || '',
815
- sku: core.sku,
816
- };
817
- }) || [],
818
- };
819
- }
@@ -1,15 +1,14 @@
1
1
  import { Product } from '@searchspring/snap-store-mobx';
2
2
  import { AbstractController } from '../Abstract/AbstractController';
3
3
  import { ControllerTypes } from '../types';
4
- import type { RecommendationStore } from '@searchspring/snap-store-mobx';
4
+ import type { Banner, RecommendationStore } from '@searchspring/snap-store-mobx';
5
5
  import type { RecommendRequestModel } from '@searchspring/snap-client';
6
6
  import type { RecommendationControllerConfig, ControllerServices, ContextVariables } from '../types';
7
7
  type RecommendationTrackMethods = {
8
8
  product: {
9
- clickThrough: (e: MouseEvent, result: Product) => void;
10
- click: (e: MouseEvent, result: Product) => void;
11
- render: (result: Product) => void;
12
- impression: (result: Product) => void;
9
+ clickThrough: (e: MouseEvent, result: Product | Banner) => void;
10
+ click: (e: MouseEvent, result: Product | Banner) => void;
11
+ impression: (result: Product | Banner) => void;
13
12
  addToCart: (result: Product) => void;
14
13
  };
15
14
  };
@@ -17,14 +16,7 @@ export declare class RecommendationController extends AbstractController {
17
16
  type: ControllerTypes;
18
17
  store: RecommendationStore;
19
18
  config: RecommendationControllerConfig;
20
- events: {
21
- product: Record<string, {
22
- click?: boolean;
23
- clickThrough?: boolean;
24
- impression?: boolean;
25
- render?: boolean;
26
- }>;
27
- };
19
+ private events;
28
20
  constructor(config: RecommendationControllerConfig, { client, store, urlManager, eventManager, profiler, logger, tracker }: ControllerServices, context?: ContextVariables);
29
21
  track: RecommendationTrackMethods;
30
22
  get params(): RecommendRequestModel;
@@ -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;AAQ3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACzE,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,KAAK,IAAI,CAAC;QACvD,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAChD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,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,MAAM,EAAE;QACP,OAAO,EAAE,MAAM,CACd,MAAM,EACN;YACC,KAAK,CAAC,EAAE,OAAO,CAAC;YAChB,YAAY,CAAC,EAAE,OAAO,CAAC;YACvB,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,CAAC;SACjB,CACD,CAAC;KACF,CAEC;gBAGD,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;IAgE3B,KAAK,EAAE,0BAA0B,CAuD/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,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;IAoD3B,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"}
@@ -2,7 +2,6 @@ import deepmerge from 'deepmerge';
2
2
  import { ErrorType } from '@searchspring/snap-store-mobx';
3
3
  import { AbstractController } from '../Abstract/AbstractController';
4
4
  import { ControllerTypes } from '../types';
5
- import { ItemTypeEnum, } from '@searchspring/beacon';
6
5
  import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
7
6
  const defaultConfig = {
8
7
  id: 'recommend',
@@ -15,59 +14,92 @@ export class RecommendationController extends AbstractController {
15
14
  constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
16
15
  super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
17
16
  this.type = ControllerTypes.recommendation;
18
- this.events = {
19
- product: {},
20
- };
17
+ this.events = {};
21
18
  this.track = {
22
19
  product: {
23
20
  clickThrough: (e, result) => {
24
- if (this.events.product[result.id]?.clickThrough)
21
+ const responseId = result.responseId;
22
+ if (this.events[responseId]?.product[result.id]?.productClickThrough)
25
23
  return;
26
- const data = getRecommendationsSchemaData({ store: this.store, results: [result] });
24
+ const beaconResult = {
25
+ type: result.type,
26
+ uid: result.id,
27
+ parentId: result.id,
28
+ sku: result.mappings.core?.sku,
29
+ };
30
+ const data = {
31
+ tag: this.store.profile.tag,
32
+ responseId,
33
+ results: [beaconResult],
34
+ };
27
35
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
28
36
  this.tracker.events.recommendations.clickThrough({ data, siteId: this.config.globals?.siteId });
29
- this.events.product[result.id] = this.events.product[result.id] || {};
30
- this.events.product[result.id].clickThrough = true;
37
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
38
+ this.events[responseId].product[result.id].productClickThrough = true;
31
39
  },
32
40
  click: (e, result) => {
33
- if (this.events.product[result.id]?.click) {
34
- return;
35
- }
41
+ const responseId = result.responseId;
36
42
  if (result.type === 'banner') {
37
- return;
43
+ if (this.events[responseId]?.product[result.id]?.inlineBannerClickThrough) {
44
+ return;
45
+ }
46
+ this.track.product.clickThrough(e, result);
47
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
48
+ this.events[responseId].product[result.id].inlineBannerClickThrough = true;
49
+ setTimeout(() => {
50
+ this.events[responseId].product[result.id].inlineBannerClickThrough = false;
51
+ }, CLICK_DUPLICATION_TIMEOUT);
52
+ }
53
+ else if (isClickWithinProductLink(e, result)) {
54
+ if (this.events?.[responseId]?.product[result.id]?.productClickThrough) {
55
+ return;
56
+ }
57
+ this.track.product.clickThrough(e, result);
58
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
59
+ this.events[responseId].product[result.id].productClickThrough = true;
60
+ setTimeout(() => {
61
+ this.events[responseId].product[result.id].productClickThrough = false;
62
+ }, CLICK_DUPLICATION_TIMEOUT);
38
63
  }
39
- isClickWithinProductLink(e, result) && this.track.product.clickThrough(e, result);
40
- this.events.product[result.id] = this.events.product[result.id] || {};
41
- this.events.product[result.id].click = true;
42
- setTimeout(() => {
43
- this.events.product[result.id].click = false;
44
- }, CLICK_DUPLICATION_TIMEOUT);
45
64
  },
46
65
  impression: (result) => {
47
- if (this.events.product[result.id]?.impression || !this.events.product[result.id]?.render)
66
+ const responseId = result.responseId;
67
+ if (this.events[responseId]?.product[result.id]?.impression) {
48
68
  return;
49
- const data = getRecommendationsSchemaData({ store: this.store, results: [result] });
69
+ }
70
+ const item = {
71
+ type: result.type,
72
+ uid: result.id,
73
+ parentId: result.id,
74
+ sku: result.mappings.core?.sku,
75
+ };
76
+ const data = {
77
+ tag: this.store.profile.tag,
78
+ responseId,
79
+ results: [item],
80
+ banners: [],
81
+ };
50
82
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
51
83
  this.tracker.events.recommendations.impression({ data, siteId: this.config.globals?.siteId });
52
- this.events.product[result.id] = this.events.product[result.id] || {};
53
- this.events.product[result.id].impression = true;
54
- return data;
55
- },
56
- render: (result) => {
57
- if (this.events.product[result.id]?.render)
58
- return;
59
- const data = getRecommendationsSchemaData({ store: this.store, results: [result] });
60
- this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
61
- this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
62
- this.events.product[result.id] = this.events.product[result.id] || {};
63
- this.events.product[result.id].render = true;
64
- return data;
84
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
85
+ this.events[responseId].product[result.id].impression = true;
65
86
  },
66
87
  addToCart: (result) => {
67
- const data = getRecommendationsAddtocartSchemaData({ store: this.store, results: [result] });
88
+ const responseId = result.responseId;
89
+ const product = {
90
+ parentId: result.id,
91
+ uid: result.id,
92
+ sku: result.mappings.core?.sku,
93
+ qty: result.quantity || 1,
94
+ price: Number(result.mappings.core?.price),
95
+ };
96
+ const data = {
97
+ responseId,
98
+ tag: this.store.profile.tag,
99
+ results: [product],
100
+ };
68
101
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
69
102
  this.tracker.events.recommendations.addToCart({ data, siteId: this.config.globals?.siteId });
70
- return data;
71
103
  },
72
104
  },
73
105
  };
@@ -77,8 +109,6 @@ export class RecommendationController extends AbstractController {
77
109
  await this.init();
78
110
  }
79
111
  const params = this.params;
80
- // reset events for new search
81
- this.events = { product: {} };
82
112
  this.store.loading = true;
83
113
  try {
84
114
  await this.eventManager.fire('beforeSearch', {
@@ -100,6 +130,8 @@ export class RecommendationController extends AbstractController {
100
130
  const response = await this.client.recommend(params);
101
131
  searchProfile.stop();
102
132
  this.log.profile(searchProfile);
133
+ const responseId = response.responseId;
134
+ this.events[responseId] = this.events[responseId] || { product: {} };
103
135
  const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
104
136
  try {
105
137
  await this.eventManager.fire('afterSearch', {
@@ -217,23 +249,12 @@ export class RecommendationController extends AbstractController {
217
249
  this.eventManager.on('afterStore', async (search, next) => {
218
250
  await next();
219
251
  const controller = search.controller;
252
+ const responseId = search.response.responseId;
220
253
  if (controller.store.loaded && !controller.store.error) {
221
- const products = controller.store.results.filter((result) => result.type === 'product');
222
- if (products.length === 0 && !search.response._cached) {
223
- // handle no results
224
- const data = getRecommendationsSchemaData({ store: this.store });
225
- this.eventManager.fire('track.product.render', { controller: this, trackEvent: data });
254
+ if (!search.response._cached) {
255
+ const data = { responseId, tag: controller.store.profile.tag };
226
256
  this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
227
257
  }
228
- products.forEach((result) => {
229
- if (!search.response._cached) {
230
- this.track.product.render(result);
231
- }
232
- else {
233
- this.events.product[result.id] = this.events.product[result.id] || {};
234
- this.events.product[result.id].render = true;
235
- }
236
- });
237
258
  }
238
259
  });
239
260
  // add 'afterStore' middleware
@@ -275,32 +296,3 @@ export class RecommendationController extends AbstractController {
275
296
  return params;
276
297
  }
277
298
  }
278
- function getRecommendationsAddtocartSchemaData({ store, results, }) {
279
- return {
280
- tag: store.profile.tag,
281
- results: results?.map((result) => {
282
- const core = result.mappings.core;
283
- return {
284
- uid: core?.uid || '',
285
- sku: core?.sku,
286
- price: Number(core?.price),
287
- qty: result.quantity || 1,
288
- };
289
- }) || [],
290
- };
291
- }
292
- function getRecommendationsSchemaData({ store, results }) {
293
- return {
294
- tag: store.profile.tag,
295
- results: results?.map((result) => {
296
- const core = result.mappings.core;
297
- const position = result.position;
298
- return {
299
- type: ItemTypeEnum.Product,
300
- position,
301
- uid: core.uid || '',
302
- sku: core.sku,
303
- };
304
- }) || [],
305
- };
306
- }
@@ -1,18 +1,25 @@
1
1
  import { AbstractController } from '../Abstract/AbstractController';
2
- import { StorageStore } from '@searchspring/snap-store-mobx';
2
+ import { StorageStore, MerchandisingContentBanner } from '@searchspring/snap-store-mobx';
3
3
  import { ControllerTypes } from '../types';
4
4
  import type { Product, Banner, SearchStore } from '@searchspring/snap-store-mobx';
5
5
  import type { SearchControllerConfig, ControllerServices, ContextVariables } from '../types';
6
6
  import { type SearchRequestModel } from '@searchspring/snapi-types';
7
7
  type SearchTrackMethods = {
8
+ banner: {
9
+ click: (e: MouseEvent, merchandisingBanner: MerchandisingContentBanner) => void;
10
+ clickThrough: (e: MouseEvent, merchandisingBanner: MerchandisingContentBanner) => void;
11
+ impression: (merchandisingBanner: MerchandisingContentBanner) => void;
12
+ };
8
13
  product: {
9
- clickThrough: (e: MouseEvent, result: Product) => void;
14
+ clickThrough: (e: MouseEvent, result: Product | Banner) => void;
10
15
  click: (e: MouseEvent, result: Product | Banner) => void;
11
- render: (result: Product) => void;
12
- impression: (result: Product) => void;
16
+ impression: (result: Product | Banner) => void;
13
17
  addToCart: (results: Product) => void;
14
18
  };
15
- redirect: (redirectURL: string) => void;
19
+ redirect: ({ redirectURL, responseId }: {
20
+ redirectURL: string;
21
+ responseId: string;
22
+ }) => void;
16
23
  };
17
24
  export declare class SearchController extends AbstractController {
18
25
  type: ControllerTypes;
@@ -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,MAAM,+BAA+B,CAAC;AAExE,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;AAmCnC,KAAK,kBAAkB,GAAG;IACzB,OAAO,EAAE;QACR,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACvD,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,KAAK,IAAI,CAAC;QACzD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QAClC,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;QACtC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AAGF,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,CAUM;gBAGnB,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;IAmQ3B,KAAK,EAAE,kBAAkB,CA+FvB;IAEF,IAAI,MAAM,IAAI,kBAAkB,CA8C/B;IAED,MAAM,QAAa,QAAQ,IAAI,CAAC,CAyN9B;IAEF,SAAS,cAAqB,OAAO,EAAE,GAAG,OAAO,KAAG,QAAQ,IAAI,CAAC,CAQ/D;CACF;AAwBD,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;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;IAsP3B,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"}