@searchspring/snap-controller 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +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
+ const BACKGROUND_FILTER_FIELD_MATCHES = ['collection', 'category', 'categories', 'hierarchy'];
8
+ const BACKGROUND_FILTERS_VALUE_FLAGS = [1, 0, '1', '0', 'true', 'false', true, false];
7
9
  const defaultConfig = {
8
10
  id: 'search',
9
11
  globals: {},
@@ -25,11 +27,16 @@ export class SearchController extends AbstractController {
25
27
  super(config, { client, store, urlManager, eventManager, profiler, logger, tracker }, context);
26
28
  this.type = ControllerTypes.search;
27
29
  this.previousResults = [];
30
+ this.pageType = 'search';
31
+ this.events = { product: {} };
28
32
  this.track = {
29
33
  product: {
30
- click: (e, result) => {
34
+ clickThrough: (e, result) => {
35
+ if (this.events.product[result.id]?.clickThrough) {
36
+ return;
37
+ }
31
38
  const target = e.target;
32
- const resultHref = result.display?.mappings.core?.url || result.mappings.core?.url;
39
+ const resultHref = result.display?.mappings.core?.url || result.mappings.core?.url || '';
33
40
  const elemHref = target?.getAttribute('href');
34
41
  // the href that should be used for restoration - if the elemHref contains the resultHref - use resultHref
35
42
  const storedHref = elemHref?.indexOf(resultHref) != -1 ? resultHref : elemHref || resultHref;
@@ -54,17 +61,59 @@ export class SearchController extends AbstractController {
54
61
  }
55
62
  // store position data or empty object
56
63
  this.storage.set('scrollMap', scrollMap);
57
- // track
58
- const { intellisuggestData, intellisuggestSignature } = result.attributes;
59
- const event = this.tracker.track.product.click({
60
- intellisuggestData,
61
- intellisuggestSignature,
62
- href: elemHref || resultHref,
64
+ const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
65
+ this.tracker.events[this.pageType].clickThrough({ data, siteId: this.config.globals?.siteId });
66
+ this.events.product[result.id] = this.events.product[result.id] || {};
67
+ this.events.product[result.id].clickThrough = true;
68
+ this.eventManager.fire('track.product.clickThrough', { controller: this, event: e, products: [result], trackEvent: data });
69
+ },
70
+ click: (e, result) => {
71
+ if (result.type === 'banner') {
72
+ return;
73
+ }
74
+ // TODO: closest might be going too far - write own function to only go n levels up - additionally check that href includes result.url
75
+ const href = e.target?.getAttribute('href') || e.target?.closest('a')?.getAttribute('href');
76
+ if (href) {
77
+ this.track.product.clickThrough(e, result);
78
+ }
79
+ else {
80
+ // TODO: in future, send as an interaction event
81
+ }
82
+ },
83
+ render: (result) => {
84
+ if (this.events.product[result.id]?.render) {
85
+ return;
86
+ }
87
+ const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
88
+ this.tracker.events[this.pageType].render({ data, siteId: this.config.globals?.siteId });
89
+ this.events.product[result.id] = this.events.product[result.id] || {};
90
+ this.events.product[result.id].render = true;
91
+ this.eventManager.fire('track.product.render', { controller: this, products: [result], trackEvent: data });
92
+ },
93
+ impression: (result) => {
94
+ if (this.events.product[result.id]?.impression) {
95
+ return;
96
+ }
97
+ const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
98
+ this.tracker.events[this.pageType].impression({ data, siteId: this.config.globals?.siteId });
99
+ this.events.product[result.id] = this.events.product[result.id] || {};
100
+ this.events.product[result.id].impression = true;
101
+ this.eventManager.fire('track.product.impression', { controller: this, products: [result], trackEvent: data });
102
+ },
103
+ addToCart: (result) => {
104
+ const data = getSearchSchemaData({ params: this.params, store: this.store, results: [result] });
105
+ this.tracker.events[this.pageType].addToCart({
106
+ data,
107
+ siteId: this.config.globals?.siteId,
63
108
  });
64
- this.eventManager.fire('track.product.click', { controller: this, event: e, result, trackEvent: event });
65
- return event;
109
+ this.eventManager.fire('track.product.addToCart', { controller: this, products: [result], trackEvent: data });
66
110
  },
67
111
  },
112
+ redirect: (redirectURL) => {
113
+ const data = getSearchRedirectSchemaData({ redirectURL });
114
+ this.tracker.events.search.redirect({ data, siteId: this.config.globals?.siteId });
115
+ this.eventManager.fire('track.product.redirect', { controller: this, redirectURL, trackEvent: data });
116
+ },
68
117
  };
69
118
  this.search = async () => {
70
119
  try {
@@ -72,9 +121,11 @@ export class SearchController extends AbstractController {
72
121
  await this.init();
73
122
  }
74
123
  const params = this.params;
75
- if (this.params.search?.query?.string && this.params.search?.query?.string.length) {
124
+ // reset events for new search
125
+ this.events = { product: {} };
126
+ if (params.search?.query?.string && params.search?.query?.string.length) {
76
127
  // save it to the history store
77
- this.store.history.save(this.params.search.query.string);
128
+ this.store.history.save(params.search.query.string);
78
129
  }
79
130
  this.store.loading = true;
80
131
  try {
@@ -257,6 +308,10 @@ export class SearchController extends AbstractController {
257
308
  this.store.loading = false;
258
309
  }
259
310
  };
311
+ this.addToCart = async (product) => {
312
+ this.track.product.addToCart(product);
313
+ this.eventManager.fire('addToCart', { controller: this, products: [product] });
314
+ };
260
315
  // deep merge config with defaults
261
316
  this.config = deepmerge(defaultConfig, this.config);
262
317
  // set restorePosition to be enabled by default when using infinite (if not provided)
@@ -270,6 +325,41 @@ export class SearchController extends AbstractController {
270
325
  });
271
326
  // set last params to undefined for compare in search
272
327
  this.storage.set('lastStringyParams', undefined);
328
+ this.eventManager.on('beforeSearch', async ({ request }, next) => {
329
+ // wait for other middleware to resolve
330
+ await next();
331
+ if (this.context?.pageType === 'category') {
332
+ this.pageType = 'category';
333
+ return;
334
+ }
335
+ const req = request;
336
+ const query = req.search?.query;
337
+ if (!query) {
338
+ const hasCategoryBackgroundFilters = req.filters
339
+ ?.filter((filter) => filter.background)
340
+ .filter((filter) => {
341
+ return BACKGROUND_FILTER_FIELD_MATCHES.find((bgFilter) => {
342
+ return filter.field?.toLowerCase().includes(bgFilter);
343
+ });
344
+ })
345
+ .filter((filter) => {
346
+ return BACKGROUND_FILTERS_VALUE_FLAGS.every((flag) => {
347
+ switch (filter.type) {
348
+ case 'range':
349
+ const rangeFilter = filter;
350
+ return rangeFilter.value !== flag;
351
+ case 'value':
352
+ default:
353
+ const valueFilter = filter;
354
+ return valueFilter.value !== flag;
355
+ }
356
+ });
357
+ });
358
+ if (hasCategoryBackgroundFilters?.length) {
359
+ this.pageType = 'category';
360
+ }
361
+ }
362
+ });
273
363
  // add 'afterSearch' middleware
274
364
  this.eventManager.on('afterSearch', async (search, next) => {
275
365
  const config = search.controller.config;
@@ -278,6 +368,7 @@ export class SearchController extends AbstractController {
278
368
  if (redirectURL && config?.settings?.redirects?.merchandising && !search?.response?.filters?.length && !searchStore.loaded) {
279
369
  //set loaded to true to prevent infinite search/reloading from happening
280
370
  searchStore.loaded = true;
371
+ this.track.redirect(redirectURL);
281
372
  window.location.replace(redirectURL);
282
373
  return false;
283
374
  }
@@ -393,17 +484,15 @@ export class SearchController extends AbstractController {
393
484
  }
394
485
  params.tracking = params.tracking || {};
395
486
  params.tracking.domain = window.location.href;
396
- const userId = this.tracker.getUserId();
487
+ const { userId, sessionId, pageLoadId, shopperId } = this.tracker.getContext();
397
488
  if (userId) {
398
489
  params.tracking.userId = userId;
399
490
  }
400
- const sessionId = this.tracker.getContext().sessionId;
401
491
  if (sessionId) {
402
492
  params.tracking.sessionId = sessionId;
403
493
  }
404
- const pageId = this.tracker.getContext().pageLoadId;
405
- if (pageId) {
406
- params.tracking.pageLoadId = pageId;
494
+ if (pageLoadId) {
495
+ params.tracking.pageLoadId = pageLoadId;
407
496
  }
408
497
  if (!this.config.globals?.personalization?.disabled) {
409
498
  const cartItems = this.tracker.cookies.cart.get();
@@ -416,7 +505,6 @@ export class SearchController extends AbstractController {
416
505
  params.personalization = params.personalization || {};
417
506
  params.personalization.lastViewed = lastViewedItems.join(',');
418
507
  }
419
- const shopperId = this.tracker.getShopperId();
420
508
  if (shopperId) {
421
509
  params.personalization = params.personalization || {};
422
510
  params.personalization.shopper = shopperId;
@@ -467,3 +555,69 @@ export function generateHrefSelector(element, href, levels = 7) {
467
555
  }
468
556
  return;
469
557
  }
558
+ function getSearchRedirectSchemaData({ redirectURL }) {
559
+ return {
560
+ redirect: redirectURL,
561
+ };
562
+ }
563
+ function getSearchSchemaData({ params, store, results, }) {
564
+ const filters = params.filters?.reduce((acc, filter) => {
565
+ const key = filter.background ? 'bgfilter' : 'filter';
566
+ acc[key] = acc[key] || [];
567
+ const value = filter.type === 'range' &&
568
+ !isNaN(filter.value?.low) &&
569
+ !isNaN(filter.value?.low)
570
+ ? [`low=${filter.value?.low}`, `high=${filter.value?.high}`]
571
+ : [`${filter.value}`];
572
+ const existing = acc[key].find((item) => item.field === filter.field);
573
+ if (existing && !existing.value.includes(value[0])) {
574
+ existing.value.push(...value);
575
+ }
576
+ else {
577
+ acc[key].push({
578
+ field: filter.field,
579
+ value,
580
+ });
581
+ }
582
+ return acc;
583
+ }, {});
584
+ return {
585
+ q: params.search?.query?.string || '',
586
+ correctedQuery: params.search?.originalQuery,
587
+ ...filters,
588
+ sort: [
589
+ {
590
+ field: store.sorting.current?.field,
591
+ dir: store.sorting.current?.direction,
592
+ },
593
+ ],
594
+ pagination: {
595
+ totalResults: store.pagination.totalResults,
596
+ page: store.pagination.page,
597
+ resultsPerPage: store.pagination.pageSize,
598
+ },
599
+ merchandising: {
600
+ personalized: store.merchandising.personalized,
601
+ redirect: store.merchandising.redirect,
602
+ triggeredCampaigns: (store.merchandising.campaigns?.length &&
603
+ store.merchandising.campaigns?.map((campaign) => {
604
+ const experiement = store.merchandising.experiments.find((experiment) => experiment.campaignId === campaign.id);
605
+ return {
606
+ id: campaign.id,
607
+ experimentId: experiement?.experimentId,
608
+ variationId: experiement?.variationId,
609
+ };
610
+ })) ||
611
+ undefined,
612
+ },
613
+ results: results?.map((result) => {
614
+ const core = result.mappings.core;
615
+ return {
616
+ uid: core.uid || '',
617
+ // childUid: core.uid,
618
+ sku: core.sku,
619
+ // childSku: core.sku,
620
+ };
621
+ }) || [],
622
+ };
623
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@searchspring/snap-controller",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
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.64.0",
23
+ "@searchspring/snap-toolbox": "^0.65.0",
24
24
  "css.escape": "1.5.1",
25
25
  "deepmerge": "4.3.1"
26
26
  },
27
27
  "devDependencies": {
28
- "@searchspring/snap-client": "^0.64.0",
29
- "@searchspring/snap-event-manager": "^0.64.0",
30
- "@searchspring/snap-logger": "^0.64.0",
31
- "@searchspring/snap-profiler": "^0.64.0",
32
- "@searchspring/snap-store-mobx": "^0.64.0",
33
- "@searchspring/snap-tracker": "^0.64.0",
34
- "@searchspring/snap-url-manager": "^0.64.0"
28
+ "@searchspring/snap-client": "^0.65.0",
29
+ "@searchspring/snap-event-manager": "^0.65.0",
30
+ "@searchspring/snap-logger": "^0.65.0",
31
+ "@searchspring/snap-profiler": "^0.65.0",
32
+ "@searchspring/snap-store-mobx": "^0.65.0",
33
+ "@searchspring/snap-tracker": "^0.65.0",
34
+ "@searchspring/snap-url-manager": "^0.65.0"
35
35
  },
36
36
  "sideEffects": false,
37
37
  "files": [
38
38
  "dist/**/*"
39
39
  ],
40
- "gitHead": "fc0fb9a2fe535b394a2f0d340a20b2b72ab84bff"
40
+ "gitHead": "e96fc4426a64178a94a8ca603ec834bdca020fdb"
41
41
  }