@searchspring/snap-controller 0.72.2 → 0.73.6

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 +152 -149
  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 -94
  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 -170
  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 +132 -141
  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 -82
  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 -167
  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,138 @@ 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 banner = { uid };
51
+ const data = {
52
+ responseId,
53
+ banners: [banner],
54
+ results: [],
55
+ };
56
+ this.eventManager.fire('track.banner.impression', { controller: this, product: { uid }, trackEvent: data });
57
+ this.tracker.events.autocomplete.impression({ data, siteId: this.config.globals?.siteId });
58
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
59
+ this.events[responseId].banner[uid].impression = true;
60
+ },
61
+ click: (e, banner) => {
62
+ const { responseId, uid } = banner;
63
+ if (isClickWithinBannerLink(e)) {
64
+ if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
65
+ return;
66
+ }
67
+ this.track.banner.clickThrough(e, banner);
68
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
69
+ this.events[responseId].banner[uid].clickThrough = true;
70
+ setTimeout(() => {
71
+ this.events[responseId].banner[uid].clickThrough = false;
72
+ }, CLICK_DUPLICATION_TIMEOUT);
73
+ }
74
+ },
75
+ clickThrough: (e, { uid, responseId }) => {
76
+ const banner = { uid };
77
+ const data = {
78
+ responseId,
79
+ banners: [banner],
80
+ };
81
+ 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 });
83
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
84
+ this.events[responseId].banner[uid].clickThrough = true;
85
+ setTimeout(() => {
86
+ this.events[responseId].banner[uid].clickThrough = false;
87
+ }, CLICK_DUPLICATION_TIMEOUT);
88
+ },
89
+ },
90
+ product: {
91
+ clickThrough: (e, result) => {
92
+ const responseId = result.responseId;
93
+ const item = {
94
+ type: result.type,
95
+ uid: result.id,
96
+ parentId: result.id,
97
+ sku: result.mappings.core?.sku,
98
+ };
99
+ const data = {
100
+ responseId,
101
+ results: [item],
102
+ };
52
103
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
53
104
  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
105
  },
57
106
  click: (e, result) => {
58
- if (this.events.product[result.id]?.click) {
59
- return;
107
+ const responseId = result.responseId;
108
+ if (result.type === 'banner' && isClickWithinBannerLink(e)) {
109
+ if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
110
+ return;
111
+ }
112
+ this.track.product.clickThrough(e, result);
113
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
114
+ this.events[responseId].product[result.id].inlineBannerClickThrough = true;
115
+ setTimeout(() => {
116
+ this.events[responseId].product[result.id].inlineBannerClickThrough = false;
117
+ }, CLICK_DUPLICATION_TIMEOUT);
60
118
  }
61
- if (result.type === 'banner') {
62
- return;
119
+ else if (isClickWithinProductLink(e, result)) {
120
+ if (this.events?.[responseId]?.product[result.id]?.productClickThrough) {
121
+ return;
122
+ }
123
+ this.track.product.clickThrough(e, result);
124
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
125
+ this.events[responseId].product[result.id].productClickThrough = true;
126
+ setTimeout(() => {
127
+ this.events[responseId].product[result.id].productClickThrough = false;
128
+ }, CLICK_DUPLICATION_TIMEOUT);
63
129
  }
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
130
  },
80
131
  impression: (result) => {
81
- if (this.events.product[result.id]?.impression || !this.events.product[result.id]?.render)
132
+ const responseId = result.responseId;
133
+ if (this.events?.[responseId]?.product[result.id]?.impression) {
82
134
  return;
83
- const data = getAutocompleteSchemaData({ params: this.params, store: this.store, results: [result] });
135
+ }
136
+ const item = {
137
+ type: result.type,
138
+ uid: result.id,
139
+ parentId: result.id,
140
+ sku: result.mappings.core?.sku,
141
+ };
142
+ const data = {
143
+ responseId,
144
+ results: [item],
145
+ banners: [],
146
+ };
84
147
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
85
148
  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;
149
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
150
+ this.events[responseId].product[result.id].impression = true;
88
151
  },
89
152
  addToCart: (result) => {
90
- const data = getAutocompleteAddtocartSchemaData({ params: this.params, store: this.store, results: [result] });
153
+ const responseId = result.responseId;
154
+ const product = {
155
+ parentId: result.id,
156
+ uid: result.id,
157
+ sku: result.mappings.core?.sku,
158
+ qty: result.quantity || 1,
159
+ price: Number(result.mappings.core?.price),
160
+ };
161
+ const data = {
162
+ responseId,
163
+ results: [product],
164
+ };
91
165
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
92
166
  this.tracker.events.autocomplete.addToCart({ data, siteId: this.config.globals?.siteId });
93
167
  },
94
168
  },
95
- redirect: (redirectURL) => {
96
- const data = getAutocompleteRedirectSchemaData({ redirectURL });
169
+ redirect: ({ redirectURL, responseId }) => {
170
+ const data = {
171
+ responseId,
172
+ redirect: redirectURL,
173
+ };
97
174
  this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
98
175
  this.tracker.events.autocomplete.redirect({ data, siteId: this.config.globals?.siteId });
99
176
  },
@@ -230,6 +307,7 @@ export class AutocompleteController extends AbstractController {
230
307
  if (((!this.store.state.input && !value) || this.store.state.input == value) && this.store.loaded) {
231
308
  return;
232
309
  }
310
+ this.store.state.source = 'input';
233
311
  this.store.state.input = value;
234
312
  // remove merch redirect to prevent race condition
235
313
  this.store.merchandising.redirect = '';
@@ -345,17 +423,20 @@ export class AutocompleteController extends AbstractController {
345
423
  }
346
424
  searchProfile.stop();
347
425
  this.log.profile(searchProfile);
426
+ const responseId = response.tracking.responseId;
427
+ this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
348
428
  if (response.search?.query === this.lastSearchQuery) {
349
- const impressedResultIds = Object.keys(this.events.product).filter((resultId) => this.events.product[resultId]?.impression);
350
- this.events = {
429
+ const impressedResultIds = Object.keys(this.events[responseId].product || {}).filter((resultId) => this.events[responseId].product?.[resultId]?.impression);
430
+ this.events[responseId] = {
351
431
  product: impressedResultIds.reduce((acc, resultId) => {
352
432
  acc[resultId] = { impression: true };
353
433
  return acc;
354
434
  }, {}),
435
+ banner: this.events[responseId].banner,
355
436
  };
356
437
  }
357
438
  else {
358
- this.events = { product: {} };
439
+ this.events[responseId] = { product: {}, banner: {} };
359
440
  this.lastSearchQuery = response.search?.query;
360
441
  }
361
442
  const afterSearchProfile = this.profiler.create({ type: 'event', name: 'afterSearch', context: params }).start();
@@ -475,23 +556,10 @@ export class AutocompleteController extends AbstractController {
475
556
  this.eventManager.on('afterStore', async (search, next) => {
476
557
  await next();
477
558
  const controller = search.controller;
559
+ const responseId = search.response.tracking.responseId;
478
560
  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 });
484
- this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
485
- }
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
- });
561
+ const data = { responseId };
562
+ this.tracker.events.autocomplete.render({ data, siteId: this.config.globals?.siteId });
495
563
  }
496
564
  });
497
565
  // add 'afterSearch' middleware
@@ -510,7 +578,7 @@ export class AutocompleteController extends AbstractController {
510
578
  const inputState = ac.controller.store.state.input;
511
579
  const redirectURL = ac.controller.store.merchandising?.redirect;
512
580
  if (this.config?.settings?.redirects?.merchandising && inputState && redirectURL) {
513
- this.track.redirect(redirectURL);
581
+ this.track.redirect({ redirectURL, responseId: ac.controller.store.merchandising?.responseId });
514
582
  window.location.href = redirectURL;
515
583
  return false;
516
584
  }
@@ -543,6 +611,14 @@ export class AutocompleteController extends AbstractController {
543
611
  if (pageLoadId) {
544
612
  params.tracking.pageLoadId = pageLoadId;
545
613
  }
614
+ if (this.store.state.input) {
615
+ params.search = params.search || {};
616
+ params.search.input = this.store.state.input;
617
+ }
618
+ if (this.store.state.source) {
619
+ params.search = params.search || {};
620
+ params.search.source = this.store.state.source;
621
+ }
546
622
  if (!this.config.globals?.personalization?.disabled) {
547
623
  const cartItems = this.tracker.cookies.cart.get();
548
624
  if (cartItems.length) {
@@ -732,88 +808,3 @@ function unbindFormParameters(form, fn) {
732
808
  }
733
809
  }
734
810
  }
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;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"}
@@ -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,10 @@ 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 });
226
- this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
227
- }
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
- });
254
+ const data = { responseId, tag: controller.store.profile.tag };
255
+ this.tracker.events.recommendations.render({ data, siteId: this.config.globals?.siteId });
237
256
  }
238
257
  });
239
258
  // add 'afterStore' middleware
@@ -275,32 +294,3 @@ export class RecommendationController extends AbstractController {
275
294
  return params;
276
295
  }
277
296
  }
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;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"}