@searchspring/snap-controller 0.72.1 → 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
@@ -4,8 +4,8 @@ import { AbstractController } from '../Abstract/AbstractController';
4
4
  import { StorageStore, ErrorType } from '@searchspring/snap-store-mobx';
5
5
  import { getSearchParams } from '../utils/getParams';
6
6
  import { ControllerTypes } from '../types';
7
- import { ItemTypeEnum, } from '@searchspring/beacon';
8
7
  import { CLICK_DUPLICATION_TIMEOUT, isClickWithinProductLink } from '../utils/isClickWithinProductLink';
8
+ import { isClickWithinBannerLink } from '../utils/isClickWithinBannerLink';
9
9
  const BACKGROUND_FILTER_FIELD_MATCHES = ['collection', 'category', 'categories', 'hierarchy', 'brand', 'manufacturer'];
10
10
  const BACKGROUND_FILTERS_VALUE_FLAGS = [1, 0, '1', '0', 'true', 'false', true, false];
11
11
  const defaultConfig = {
@@ -24,7 +24,6 @@ const defaultConfig = {
24
24
  },
25
25
  },
26
26
  };
27
- const schemaMap = {};
28
27
  export class SearchController extends AbstractController {
29
28
  constructor(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context) {
30
29
  super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
@@ -33,13 +32,56 @@ export class SearchController extends AbstractController {
33
32
  this.page = {
34
33
  type: 'search',
35
34
  };
36
- this.events = { product: {} };
35
+ this.events = {};
37
36
  this.track = {
38
- product: {
39
- clickThrough: (e, result) => {
40
- if (this.events.product[result.id]?.clickThrough) {
37
+ banner: {
38
+ impression: ({ uid, responseId }) => {
39
+ if (this.events[responseId]?.banner[uid]?.impression) {
41
40
  return;
42
41
  }
42
+ const banner = { uid };
43
+ const data = {
44
+ responseId,
45
+ banners: [banner],
46
+ results: [],
47
+ };
48
+ 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 });
50
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
51
+ this.events[responseId].banner[uid].impression = true;
52
+ },
53
+ click: (e, banner) => {
54
+ const { responseId, uid } = banner;
55
+ if (isClickWithinBannerLink(e)) {
56
+ if (this.events?.[responseId]?.banner[uid]?.clickThrough) {
57
+ return;
58
+ }
59
+ this.track.banner.clickThrough(e, banner);
60
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
61
+ this.events[responseId].banner[uid].clickThrough = true;
62
+ setTimeout(() => {
63
+ this.events[responseId].banner[uid].clickThrough = false;
64
+ }, CLICK_DUPLICATION_TIMEOUT);
65
+ }
66
+ },
67
+ clickThrough: (e, { uid, responseId }) => {
68
+ const banner = { uid };
69
+ const data = {
70
+ responseId,
71
+ banners: [banner],
72
+ };
73
+ 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 });
75
+ this.events[responseId].banner[uid] = this.events[responseId].banner[uid] || {};
76
+ this.events[responseId].banner[uid].clickThrough = true;
77
+ setTimeout(() => {
78
+ this.events[responseId].banner[uid].clickThrough = false;
79
+ }, CLICK_DUPLICATION_TIMEOUT);
80
+ },
81
+ },
82
+ product: {
83
+ clickThrough: (e, result) => {
84
+ const responseId = result.responseId;
43
85
  const target = e.target;
44
86
  const resultHref = result.display?.mappings.core?.url || result.mappings.core?.url || '';
45
87
  const elemHref = target?.getAttribute('href');
@@ -66,58 +108,88 @@ export class SearchController extends AbstractController {
66
108
  }
67
109
  // store position data or empty object
68
110
  this.storage.set('scrollMap', scrollMap);
69
- const data = schemaMap[result.id];
111
+ const item = {
112
+ type: result.type,
113
+ uid: result.id,
114
+ parentId: result.id,
115
+ sku: result.mappings.core?.sku,
116
+ };
117
+ const data = {
118
+ responseId,
119
+ results: [item],
120
+ };
70
121
  this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, product: result, trackEvent: data });
71
122
  this.tracker.events[this.page.type].clickThrough({ data, siteId: this.config.globals?.siteId });
72
- this.events.product[result.id] = this.events.product[result.id] || {};
73
- this.events.product[result.id].clickThrough = true;
74
123
  },
75
124
  click: (e, result) => {
76
- if (this.events.product[result.id]?.click) {
77
- return;
78
- }
79
- if (result.type === 'banner') {
80
- return;
125
+ const responseId = result.responseId;
126
+ if (result.type === 'banner' && isClickWithinBannerLink(e)) {
127
+ if (this.events?.[responseId]?.product[result.id]?.inlineBannerClickThrough) {
128
+ return;
129
+ }
130
+ this.track.product.clickThrough(e, result);
131
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
132
+ this.events[responseId].product[result.id].inlineBannerClickThrough = true;
133
+ setTimeout(() => {
134
+ this.events[responseId].product[result.id].inlineBannerClickThrough = false;
135
+ }, CLICK_DUPLICATION_TIMEOUT);
81
136
  }
82
- isClickWithinProductLink(e, result) && this.track.product.clickThrough(e, result);
83
- this.events.product[result.id] = this.events.product[result.id] || {};
84
- this.events.product[result.id].click = true;
85
- setTimeout(() => {
86
- this.events.product[result.id].click = false;
87
- }, CLICK_DUPLICATION_TIMEOUT);
88
- },
89
- render: (result) => {
90
- if (this.events.product[result.id]?.render) {
91
- return;
137
+ else if (isClickWithinProductLink(e, result)) {
138
+ if (this.events?.[responseId]?.product[result.id]?.productClickThrough) {
139
+ return;
140
+ }
141
+ this.track.product.clickThrough(e, result);
142
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
143
+ this.events[responseId].product[result.id].productClickThrough = true;
144
+ setTimeout(() => {
145
+ this.events[responseId].product[result.id].productClickThrough = false;
146
+ }, CLICK_DUPLICATION_TIMEOUT);
92
147
  }
93
- const data = schemaMap[result.id];
94
- this.eventManager.fire('track.product.render', { controller: this, product: result, trackEvent: data });
95
- this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
96
- this.events.product[result.id] = this.events.product[result.id] || {};
97
- this.events.product[result.id].render = true;
98
148
  },
99
149
  impression: (result) => {
100
- if (this.events.product[result.id]?.impression || !this.events.product[result.id]?.render) {
150
+ const responseId = result.responseId;
151
+ if (this.events[responseId]?.product[result.id]?.impression) {
101
152
  return;
102
153
  }
103
- const data = schemaMap[result.id];
154
+ const item = {
155
+ type: result.type,
156
+ uid: result.id,
157
+ parentId: result.id,
158
+ sku: result.mappings.core?.sku,
159
+ };
160
+ const data = {
161
+ responseId,
162
+ results: [item],
163
+ banners: [],
164
+ };
104
165
  this.eventManager.fire('track.product.impression', { controller: this, product: result, trackEvent: data });
105
166
  this.tracker.events[this.page.type].impression({ data, siteId: this.config.globals?.siteId });
106
- this.events.product[result.id] = this.events.product[result.id] || {};
107
- this.events.product[result.id].impression = true;
167
+ this.events[responseId].product[result.id] = this.events[responseId].product[result.id] || {};
168
+ this.events[responseId].product[result.id].impression = true;
108
169
  },
109
170
  addToCart: (result) => {
110
- const data = getSearchAddtocartSchemaData({ searchSchemaData: schemaMap[result.id], results: [result] });
171
+ const responseId = result.responseId;
172
+ const product = {
173
+ parentId: result.id,
174
+ uid: result.id,
175
+ sku: result.mappings.core?.sku,
176
+ qty: result.quantity || 1,
177
+ price: Number(result.mappings.core?.price),
178
+ };
179
+ const data = {
180
+ responseId,
181
+ results: [product],
182
+ };
111
183
  this.eventManager.fire('track.product.addToCart', { controller: this, product: result, trackEvent: data });
112
- this.tracker.events[this.page.type].addToCart({
113
- data,
114
- siteId: this.config.globals?.siteId,
115
- });
184
+ this.tracker.events[this.page.type].addToCart({ data, siteId: this.config.globals?.siteId });
116
185
  },
117
186
  },
118
- redirect: (redirectURL) => {
119
- const data = getSearchRedirectSchemaData({ redirectURL });
120
- this.eventManager.fire('track.product.redirect', { controller: this, redirectURL, trackEvent: data });
187
+ redirect: ({ redirectURL, responseId }) => {
188
+ const data = {
189
+ responseId,
190
+ redirect: redirectURL,
191
+ };
192
+ this.eventManager.fire('track.redirect', { controller: this, redirectURL, trackEvent: data });
121
193
  this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
122
194
  },
123
195
  };
@@ -156,7 +228,7 @@ export class SearchController extends AbstractController {
156
228
  }
157
229
  const searchProfile = this.profiler.create({ type: 'event', name: 'search', context: params }).start();
158
230
  let meta = {};
159
- let response = {};
231
+ let response;
160
232
  // infinite scroll functionality (after page 1)
161
233
  if (this.config.settings?.infinite && params.pagination?.page && params.pagination.page > 1) {
162
234
  const preventBackfill = this.config.settings.infinite?.backfill && !this.store.results.length && params.pagination.page > this.config.settings.infinite.backfill;
@@ -185,7 +257,7 @@ export class SearchController extends AbstractController {
185
257
  }
186
258
  }
187
259
  backfillRequestsParams.push(backfillParams);
188
- return this.client.search(backfillParams);
260
+ return this.client[this.page.type](backfillParams);
189
261
  });
190
262
  const backfillResponses = await Promise.all(backfillRequests);
191
263
  // backfillResponses are [meta, searchResponse][]
@@ -193,8 +265,9 @@ export class SearchController extends AbstractController {
193
265
  meta = backfillResponses[0][0];
194
266
  response = backfillResponses[0][1];
195
267
  // accumulate results from all backfill responses
196
- const backfillResults = backfillResponses.reduce((results, response, index) => {
197
- createResultSchemaMapping({ request: backfillRequestsParams[index], response: response });
268
+ const backfillResults = backfillResponses.reduce((results, response) => {
269
+ const responseId = response[1].tracking.responseId;
270
+ this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
198
271
  return results.concat(...response[1].results);
199
272
  }, []);
200
273
  // overwrite pagination params to expected state
@@ -205,20 +278,20 @@ export class SearchController extends AbstractController {
205
278
  }
206
279
  else {
207
280
  // infinite with no backfills.
208
- [meta, response] = await this.client.search(params);
209
- createResultSchemaMapping({ request: params, response: [meta, response] });
281
+ [meta, response] = await this.client[this.page.type](params);
282
+ const responseId = response.tracking.responseId;
283
+ this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
210
284
  // append new results to previous results
211
285
  response.results = [...this.previousResults, ...(response.results || [])];
212
286
  }
213
287
  }
214
288
  else {
215
289
  // normal request
216
- // reset events for new search
217
- this.events = { product: {} };
218
290
  // clear previousResults to prevent infinite scroll from using them
219
291
  this.previousResults = [];
220
- [meta, response] = await this.client.search(params);
221
- createResultSchemaMapping({ request: params, response: [meta, response] });
292
+ [meta, response] = await this.client[this.page.type](params);
293
+ const responseId = response.tracking.responseId;
294
+ this.events[responseId] = this.events[responseId] || { product: {}, banner: {} };
222
295
  }
223
296
  // MockClient will overwrite the client search() method and use SearchData to return mock data which already contains meta data
224
297
  if (!response.meta) {
@@ -380,7 +453,7 @@ export class SearchController extends AbstractController {
380
453
  if (redirectURL && config?.settings?.redirects?.merchandising && !search?.response?.filters?.length && !searchStore.loaded) {
381
454
  //set loaded to true to prevent infinite search/reloading from happening
382
455
  searchStore.loaded = true;
383
- this.track.redirect(redirectURL);
456
+ this.track.redirect({ redirectURL, responseId: search.response.tracking.responseId });
384
457
  window.location.replace(redirectURL);
385
458
  return false;
386
459
  }
@@ -441,21 +514,12 @@ export class SearchController extends AbstractController {
441
514
  this.eventManager.on('afterStore', async (search, next) => {
442
515
  await next();
443
516
  const controller = search.controller;
517
+ const responseId = search.response.tracking.responseId;
444
518
  if (controller.store.loaded && !controller.store.error) {
445
- const products = controller.store.results.filter((result) => result.type === 'product' && !this.events.product[result.id]?.render);
446
- if (products.length === 0 && !search.response._cached) {
447
- // handle no results
448
- const data = getSearchSchemaData({ params: search.request, response: search.response });
449
- this.eventManager.fire('track.product.render', { controller: this, trackEvent: data });
519
+ if (!search.response._cached) {
520
+ const data = { responseId };
450
521
  this.tracker.events[this.page.type].render({ data, siteId: this.config.globals?.siteId });
451
522
  }
452
- products.forEach((result) => {
453
- if (!search.response._cached) {
454
- this.track.product.render(result);
455
- }
456
- this.events.product[result.id] = this.events.product[result.id] || {};
457
- this.events.product[result.id].render = true;
458
- });
459
523
  const config = search.controller.config;
460
524
  const nonBackgroundFilters = search?.request?.filters?.filter((filter) => !filter.background);
461
525
  if (config?.settings?.redirects?.singleResult &&
@@ -582,26 +646,6 @@ export class SearchController extends AbstractController {
582
646
  return params;
583
647
  }
584
648
  }
585
- function createResultSchemaMapping({ request, response }) {
586
- const [_, searchResponse] = response;
587
- const schema = getSearchSchemaData({
588
- params: request,
589
- response: searchResponse,
590
- });
591
- searchResponse.results?.forEach((result, idx) => {
592
- schemaMap[result.id] = {
593
- ...schema,
594
- results: [
595
- {
596
- type: ItemTypeEnum.Product,
597
- position: idx + 1,
598
- uid: result.mappings?.core?.uid || '',
599
- sku: result.mappings?.core?.sku,
600
- },
601
- ],
602
- };
603
- });
604
- }
605
649
  export function getStorableRequestParams(request) {
606
650
  return {
607
651
  siteId: request.siteId,
@@ -644,84 +688,3 @@ export function generateHrefSelector(element, href, levels = 7) {
644
688
  }
645
689
  return;
646
690
  }
647
- function getSearchRedirectSchemaData({ redirectURL }) {
648
- return {
649
- redirect: redirectURL,
650
- };
651
- }
652
- function getSearchAddtocartSchemaData({ searchSchemaData, results, }) {
653
- return {
654
- ...searchSchemaData,
655
- results: results?.map((result) => {
656
- const core = result.mappings.core;
657
- return {
658
- uid: core.uid || '',
659
- sku: core.sku,
660
- price: Number(core.price),
661
- qty: result.quantity || 1,
662
- };
663
- }) || [],
664
- };
665
- }
666
- function getSearchSchemaData({ params, response }) {
667
- const filters = params.filters?.reduce((acc, filter) => {
668
- const key = filter.background ? 'bgfilter' : 'filter';
669
- acc[key] = acc[key] || [];
670
- const value = filter.type === 'range' &&
671
- !isNaN(filter.value?.low) &&
672
- !isNaN(filter.value?.high)
673
- ? [`low=${filter.value?.low}`, `high=${filter.value?.high}`]
674
- : [`${filter.value}`];
675
- const existing = acc[key].find((item) => item.field === filter.field);
676
- if (existing && !existing.value.includes(value[0])) {
677
- existing.value.push(...value);
678
- }
679
- else {
680
- acc[key].push({
681
- field: filter.field,
682
- value,
683
- });
684
- }
685
- return acc;
686
- }, {});
687
- let correctedQuery;
688
- if (response?.search?.originalQuery && response?.search?.query) {
689
- correctedQuery = response?.search?.query;
690
- }
691
- const campaigns = response?.merchandising?.campaigns || [];
692
- const experiments = response?.merchandising?.experiments || [];
693
- return {
694
- q: params.search?.query?.string || '',
695
- rq: params.search?.subQuery ? params.search?.subQuery : undefined,
696
- correctedQuery,
697
- matchType: response?.search?.matchType,
698
- ...filters,
699
- sort: params.sorts?.map((sort) => {
700
- return {
701
- field: sort.field,
702
- dir: sort.direction,
703
- };
704
- }),
705
- pagination: {
706
- totalResults: response?.pagination?.totalResults,
707
- page: response?.pagination?.page,
708
- resultsPerPage: response?.pagination?.pageSize,
709
- },
710
- merchandising: {
711
- personalized: response?.merchandising?.personalized,
712
- redirect: response?.merchandising?.redirect,
713
- triggeredCampaigns: (campaigns.length &&
714
- campaigns.map((campaign) => {
715
- const experiement = experiments.find((experiment) => experiment.campaignId === campaign.id);
716
- return {
717
- id: campaign.id,
718
- experimentId: experiement?.experimentId,
719
- variationId: experiement?.variationId,
720
- };
721
- })) ||
722
- undefined,
723
- },
724
- banners: [],
725
- results: [],
726
- };
727
- }
@@ -0,0 +1,2 @@
1
+ export declare const isClickWithinBannerLink: (e: MouseEvent) => boolean;
2
+ //# sourceMappingURL=isClickWithinBannerLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isClickWithinBannerLink.d.ts","sourceRoot":"","sources":["../../../src/utils/isClickWithinBannerLink.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,uBAAuB,MAAO,UAAU,KAAG,OAcvD,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { CLICK_THROUGH_CLOSEST_MAX_LEVELS } from './isClickWithinProductLink';
2
+ const TRACKING_ATTRIBUTE = 'sstracking';
3
+ export const isClickWithinBannerLink = (e) => {
4
+ let currentElement = e.target;
5
+ let href = null;
6
+ let level = 0;
7
+ while (currentElement && (level < CLICK_THROUGH_CLOSEST_MAX_LEVELS || !currentElement.getAttribute(TRACKING_ATTRIBUTE))) {
8
+ href = currentElement.getAttribute('href');
9
+ const isAnchor = currentElement.tagName.toLowerCase() === 'a';
10
+ if (href && isAnchor) {
11
+ return true;
12
+ }
13
+ currentElement = currentElement.parentElement;
14
+ level++;
15
+ }
16
+ return false;
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searchspring/snap-controller",
3
- "version": "0.72.1",
3
+ "version": "0.73.5",
4
4
  "description": "Snap Controllers",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -20,22 +20,22 @@
20
20
  "test:watch": "jest --watch"
21
21
  },
22
22
  "dependencies": {
23
- "@searchspring/snap-toolbox": "0.72.1",
23
+ "@searchspring/snap-toolbox": "0.73.5",
24
24
  "css.escape": "1.5.1",
25
25
  "deepmerge": "4.3.1"
26
26
  },
27
27
  "devDependencies": {
28
- "@searchspring/snap-client": "0.72.1",
29
- "@searchspring/snap-event-manager": "0.72.1",
30
- "@searchspring/snap-logger": "0.72.1",
31
- "@searchspring/snap-profiler": "0.72.1",
32
- "@searchspring/snap-store-mobx": "0.72.1",
33
- "@searchspring/snap-tracker": "0.72.1",
34
- "@searchspring/snap-url-manager": "0.72.1"
28
+ "@searchspring/snap-client": "0.73.5",
29
+ "@searchspring/snap-event-manager": "0.73.5",
30
+ "@searchspring/snap-logger": "0.73.5",
31
+ "@searchspring/snap-profiler": "0.73.5",
32
+ "@searchspring/snap-store-mobx": "0.73.5",
33
+ "@searchspring/snap-tracker": "0.73.5",
34
+ "@searchspring/snap-url-manager": "0.73.5"
35
35
  },
36
36
  "sideEffects": false,
37
37
  "files": [
38
38
  "dist/**/*"
39
39
  ],
40
- "gitHead": "907fc94dca17369d89e7aa9d60bbe1acdf05a3a7"
40
+ "gitHead": "3052c4ea9cc631b0b398c72ea0f155084e15b7ee"
41
41
  }