@searchspring/snap-tracker 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.
@@ -1,203 +1,86 @@
1
1
  import deepmerge from 'deepmerge';
2
- import { v4 as uuidv4 } from 'uuid';
3
2
  import { StorageStore } from '@searchspring/snap-store-mobx';
4
- import { cookies, getFlags, version, DomTargeter, getContext, charsParams } from '@searchspring/snap-toolbox';
3
+ import { version, DomTargeter, getContext } from '@searchspring/snap-toolbox';
5
4
  import { AppMode } from '@searchspring/snap-toolbox';
6
- import { TrackEvent } from './TrackEvent';
7
- import { PixelEvent } from './PixelEvent';
5
+ import { Beacon } from '@searchspring/beacon';
8
6
  import { BeaconEvent } from './BeaconEvent';
9
7
  import { BeaconType, BeaconCategory, } from './types';
10
- export const BATCH_TIMEOUT = 200;
11
- const LEGACY_USERID_COOKIE_NAME = '_isuid';
12
- const USERID_COOKIE_NAME = 'ssUserId';
13
- const SHOPPERID_COOKIE_NAME = 'ssShopperId';
14
- const COOKIE_EXPIRATION = 31536000000; // 1 year
15
- const VIEWED_COOKIE_EXPIRATION = 220752000000; // 7 years
16
- const COOKIE_SAMESITE = 'Lax';
17
- const COOKIE_DOMAIN = (typeof window !== 'undefined' && window.location.hostname && '.' + window.location.hostname.replace(/^www\./, '')) || undefined;
18
- const SESSIONID_STORAGE_NAME = 'ssSessionIdNamespace';
19
- const LOCALSTORAGE_BEACON_POOL_NAME = 'ssBeaconPool';
20
- const CART_PRODUCTS = 'ssCartProducts';
21
- const VIEWED_PRODUCTS = 'ssViewedProducts';
22
- export const MAX_VIEWED_COUNT = 20;
23
8
  const MAX_PARENT_LEVELS = 3;
24
9
  const defaultConfig = {
25
10
  id: 'track',
26
11
  framework: 'snap',
27
12
  mode: AppMode.production,
28
13
  };
29
- export class Tracker {
14
+ export class Tracker extends Beacon {
30
15
  constructor(globals, config) {
31
- this.mode = AppMode.production;
16
+ config = deepmerge(defaultConfig, config || {});
17
+ config.initiator = `searchspring/${config.framework}/${version}`;
18
+ super(globals, config);
32
19
  this.targeters = [];
33
20
  this.track = {
34
- event: (payload) => {
35
- const event = {
36
- type: payload?.type || BeaconType.CUSTOM,
37
- category: payload?.category || BeaconCategory.CUSTOM,
38
- context: payload?.context ? deepmerge(this.context, payload.context) : this.context,
39
- event: payload.event,
40
- pid: payload?.pid || undefined,
41
- };
42
- const doNotTrack = this.doNotTrack.find((entry) => entry.type === event.type && entry.category === event.category);
43
- if (doNotTrack) {
21
+ // TODO: search where this is used and remove unwanted fields from type
22
+ error: (data, siteId) => {
23
+ if (this.doNotTrack?.includes('error') || this.mode === AppMode.development) {
44
24
  return;
45
25
  }
46
- const beaconEvent = new BeaconEvent(event, this.config);
47
- this.sendEvents([beaconEvent]);
48
- return beaconEvent;
49
- },
50
- error: (data, siteId) => {
51
26
  if (!data?.stack && !data?.message) {
52
27
  // no console log
53
28
  return;
54
29
  }
55
- let context = this.context;
56
- if (siteId) {
57
- context = deepmerge(context, {
58
- context: {
59
- website: {
60
- trackingCode: siteId,
61
- },
62
- },
63
- });
30
+ const { stack, message, details } = data;
31
+ const { pageUrl } = this.getContext();
32
+ // prevent sending of errors when on localhost or CDN
33
+ if (message?.includes('Profile is currently paused') || pageUrl.includes('//localhost') || pageUrl.includes('//snapui.searchspring.io/')) {
34
+ return;
64
35
  }
65
- const { href, filename, stack, message, colno, lineno, errortimestamp, details } = data;
66
- const payload = {
67
- type: BeaconType.ERROR,
68
- category: BeaconCategory.RUNTIME,
69
- context,
70
- event: {
71
- href: href || window.location.href,
72
- filename,
36
+ this.events.error.snap({
37
+ data: {
38
+ message: message || 'unknown',
73
39
  stack,
74
- message,
75
- colno,
76
- lineno,
77
- errortimestamp,
78
40
  details,
79
- context: data.context,
80
41
  },
81
- };
82
- // prevent sending of errors when on localhost or CDN
83
- if (payload.event.message?.includes('Profile is currently paused') ||
84
- !payload.event.href ||
85
- payload.event.href.includes('//localhost') ||
86
- payload.event.href.includes('//snapui.searchspring.io/')) {
87
- return;
88
- }
89
- return this.track.event(payload);
42
+ siteId,
43
+ });
90
44
  },
91
45
  shopper: {
92
46
  login: (data, siteId) => {
93
- // sets shopperid if logged in
94
- if (!getFlags().cookies()) {
95
- return;
96
- }
97
- if (!data.id) {
98
- console.error('tracker.shopper.login event: requires a valid shopper ID parameter. Example: tracker.shopper.login({ id: "1234" })');
47
+ if (this.doNotTrack?.includes('shopper.login')) {
99
48
  return;
100
49
  }
101
- data.id = `${data.id}`;
102
- let context = this.context;
103
- if (siteId) {
104
- context = deepmerge(context, {
105
- context: {
106
- website: {
107
- trackingCode: siteId,
108
- },
109
- },
110
- });
111
- context.shopperId = data.id;
112
- }
113
- const storedShopperId = this.getShopperId();
114
- if (storedShopperId != data.id) {
115
- // user's logged in id has changed, update shopperId cookie send login event
116
- cookies.set(SHOPPERID_COOKIE_NAME, data.id, COOKIE_SAMESITE, COOKIE_EXPIRATION, COOKIE_DOMAIN);
117
- this.context.shopperId = data.id;
118
- this.sendPreflight();
119
- const payload = {
120
- type: BeaconType.LOGIN,
121
- category: BeaconCategory.PERSONALIZATION,
122
- context,
123
- event: {
124
- userId: this.context.userId,
125
- shopperId: data.id,
126
- },
127
- };
128
- return this.track.event(payload);
129
- }
50
+ this.events.shopper.login({ data: { id: data.id }, siteId });
130
51
  },
131
52
  },
132
53
  product: {
133
54
  view: (data, siteId) => {
134
- if (!data?.uid && !data?.sku && !data?.childUid && !data?.childSku) {
135
- console.error('track.product.view event: requires a valid uid, sku and/or childUid, childSku. \nExample: track.product.view({ uid: "123", sku: "product123", childUid: "123_a", childSku: "product123_a" })');
55
+ if (this.doNotTrack?.includes('product.view')) {
136
56
  return;
137
57
  }
138
- let context = this.context;
139
- if (siteId) {
140
- context = deepmerge(context, {
141
- context: {
142
- website: {
143
- trackingCode: siteId,
144
- },
145
- },
146
- });
147
- }
148
- const payload = {
149
- type: BeaconType.PRODUCT,
150
- category: BeaconCategory.PAGEVIEW,
151
- context,
152
- event: {
153
- uid: data?.uid ? `${data.uid}` : undefined,
154
- sku: data?.sku ? `${data.sku}` : undefined,
155
- childUid: data?.childUid ? `${data.childUid}` : undefined,
156
- childSku: data?.childSku ? `${data.childSku}` : undefined,
157
- },
158
- };
159
- const event = this.track.event(payload);
160
- if (event) {
161
- // save recently viewed products to cookie
162
- const sku = data?.childSku || data?.childUid || data?.sku || data?.uid;
163
- if (sku) {
164
- const lastViewedProducts = this.cookies.viewed.get();
165
- const uniqueCartItems = Array.from(new Set([sku, ...lastViewedProducts])).map((item) => `${item}`.trim());
166
- cookies.set(VIEWED_PRODUCTS, uniqueCartItems.slice(0, MAX_VIEWED_COUNT).join(','), COOKIE_SAMESITE, VIEWED_COOKIE_EXPIRATION, COOKIE_DOMAIN);
167
- if (!lastViewedProducts.includes(sku)) {
168
- this.sendPreflight();
169
- }
170
- }
171
- // legacy tracking
172
- if (data?.sku) {
173
- // only send sku to pixel tracker if present (don't send childSku)
174
- new PixelEvent({
175
- ...payload,
176
- event: {
177
- sku: data.sku,
178
- id: data.uid,
179
- },
180
- });
181
- }
182
- return event;
58
+ let result = data;
59
+ if (!data.uid && data.sku) {
60
+ result = {
61
+ ...data,
62
+ uid: data.sku,
63
+ };
183
64
  }
65
+ this.events.product.pageView({ data: { result: result }, siteId });
184
66
  },
67
+ /**
68
+ * @deprecated tracker.track.product.click() is deprecated and will be removed. Use tracker.events['search' | 'category'].clickThrough() instead
69
+ */
185
70
  click: (data, siteId) => {
71
+ // Controllers will send product click events through tracker.beacon
72
+ // For legacy support if someone calls this, continute to 1.0 beacon just like is.js
73
+ // TODO: remove after 1.0 deprecation period
74
+ if (this.doNotTrack?.includes('product.click')) {
75
+ return;
76
+ }
186
77
  if (!data?.intellisuggestData || !data?.intellisuggestSignature) {
187
78
  console.error(`track.product.click event: object parameter requires a valid intellisuggestData and intellisuggestSignature. \nExample: track.click.product({ intellisuggestData: "eJwrTs4tNM9jYCjKTM8oYXDWdQ3TDTfUDbIwMDVjMARCYwMQSi_KTAEA9IQKWA", intellisuggestSignature: "9e46f9fd3253c267fefc298704e39084a6f8b8e47abefdee57277996b77d8e70" })`);
188
79
  return;
189
80
  }
190
- let context = this.context;
191
- if (siteId) {
192
- context = deepmerge(context, {
193
- context: {
194
- website: {
195
- trackingCode: siteId,
196
- },
197
- },
198
- });
199
- }
200
- const payload = {
81
+ const beaconContext = this.getContext();
82
+ const context = transformToLegacyContext(beaconContext, siteId || this.globals.siteId);
83
+ const event = {
201
84
  type: BeaconType.CLICK,
202
85
  category: BeaconCategory.INTERACTION,
203
86
  context,
@@ -207,359 +90,117 @@ export class Tracker {
207
90
  href: data?.href ? `${data.href}` : undefined,
208
91
  },
209
92
  };
210
- // legacy tracking
211
- new TrackEvent(payload);
212
- return this.track.event(payload);
93
+ const beaconEvent = new BeaconEvent(event, this.config);
94
+ const beaconEventData = beaconEvent.send();
95
+ return beaconEventData;
213
96
  },
214
97
  },
215
98
  cart: {
216
99
  view: (data, siteId) => {
217
- if (!Array.isArray(data?.items) || !data?.items.length) {
218
- console.error('track.view.cart event: parameter must be an array of cart items. \nExample: track.view.cart({ items: [{ id: "123", sku: "product123", childSku: "product123_a", qty: "1", price: "9.99" }] })');
100
+ if (this.doNotTrack?.includes('cart.view')) {
219
101
  return;
220
102
  }
221
- let context = this.context;
222
- if (siteId) {
223
- context = deepmerge(context, {
224
- context: {
225
- website: {
226
- trackingCode: siteId,
227
- },
228
- },
229
- });
230
- }
231
- const items = data.items.map((item, index) => {
232
- if (!item?.qty || !item?.price || (!item?.uid && !item?.sku && !item?.childUid && !item?.childSku)) {
233
- console.error(`track.view.cart event: item at index ${index} requires a valid qty, price, and (uid and/or sku and/or childUid and/or childSku.) \nExample: track.view.cart({ items: [{ uid: "123", sku: "product123", childUid: "123_a", childSku: "product123_a", qty: "1", price: "9.99" }] })`);
234
- return;
235
- }
236
- const product = {
237
- qty: `${item.qty}`,
238
- price: `${item.price}`,
239
- };
240
- if (item?.uid) {
241
- product.uid = `${item.uid}`;
242
- }
243
- if (item?.sku) {
244
- product.sku = `${item.sku}`;
245
- }
246
- if (item?.childUid) {
247
- product.childUid = `${item.childUid}`;
103
+ // uid can be optional in legacy payload but required in 2.0 spec - use sku as fallback
104
+ const results = data.items
105
+ .map((item) => {
106
+ if (!item.uid && item.sku) {
107
+ return {
108
+ ...item,
109
+ uid: item.sku,
110
+ };
248
111
  }
249
- if (item?.childSku) {
250
- product.childSku = `${item.childSku}`;
112
+ else {
113
+ return item;
251
114
  }
252
- return product;
115
+ })
116
+ .map((item) => {
117
+ // convert to Product[] - ensure qty and price are numbers
118
+ return {
119
+ ...item,
120
+ qty: Number(item.qty),
121
+ price: Number(item.price),
122
+ };
253
123
  });
254
- const payload = {
255
- type: BeaconType.CART,
256
- category: BeaconCategory.CARTVIEW,
257
- context,
258
- event: { items },
259
- };
260
- const event = this.track.event(payload);
261
- if (event) {
262
- // save cart items to cookie
263
- if (items.length) {
264
- const products = items.map((item) => item?.childSku || item?.childUid || item?.sku || item?.uid || '').filter((sku) => sku);
265
- this.cookies.cart.add(products);
266
- }
267
- // legacy tracking
268
- new PixelEvent(payload);
269
- return event;
270
- }
124
+ this.events.cart.view({ data: { results: results }, siteId });
271
125
  },
272
126
  },
273
127
  order: {
274
128
  transaction: (data, siteId) => {
275
- if (!data?.items || !Array.isArray(data.items) || !data.items.length) {
276
- console.error('track.order.transaction event: object parameter must contain `items` array of cart items. \nExample: order.transaction({ order: { id: "1001", total: "10.71", transactionTotal: "9.99", city: "Los Angeles", state: "CA", country: "US" }, items: [{ uid: "123", sku: "product123", childUid: "123_a", childSku: "product123_a", qty: "1", price: "9.99" }] })');
129
+ if (this.doNotTrack?.includes('order.transaction')) {
277
130
  return;
278
131
  }
279
- let context = this.context;
280
- if (siteId) {
281
- context = deepmerge(context, {
282
- context: {
283
- website: {
284
- trackingCode: siteId,
285
- },
286
- },
287
- });
288
- }
289
- const items = data.items.map((item, index) => {
290
- if (!item?.qty || !item?.price || (!item?.uid && !item?.sku && !item?.childUid && !item?.childSku)) {
291
- console.error(`track.order.transaction event: object parameter \`items\`: item at index ${index} requires a valid qty, price, and (id or sku and/or childSku.) \nExample: order.view({ items: [{ uid: "123", sku: "product123", childUid: "123_a", childSku: "product123_a", qty: "1", price: "9.99" }] })`);
292
- return;
293
- }
294
- const product = {
295
- qty: `${item.qty}`,
296
- price: `${item.price}`,
297
- };
298
- if (item?.uid) {
299
- product.uid = `${item.uid}`;
300
- }
301
- if (item?.sku) {
302
- product.sku = `${item.sku}`;
303
- }
304
- if (item?.childUid) {
305
- product.childUid = `${item.childUid}`;
306
- }
307
- if (item?.childSku) {
308
- product.childSku = `${item.childSku}`;
309
- }
310
- return product;
311
- });
312
- const eventPayload = {
313
- orderId: data?.order?.id ? `${data.order.id}` : undefined,
314
- total: data?.order?.total ? `${data.order.total}` : undefined,
315
- transactionTotal: data?.order?.transactionTotal ? `${data.order.transactionTotal}` : undefined,
316
- city: data?.order?.city ? `${data.order.city}` : undefined,
317
- state: data?.order?.state ? `${data.order.state}` : undefined,
318
- country: data?.order?.country ? `${data.order.country}` : undefined,
319
- items,
320
- };
321
- const payload = {
322
- type: BeaconType.ORDER,
323
- category: BeaconCategory.ORDERVIEW,
324
- context,
325
- event: eventPayload,
132
+ const order = data.order;
133
+ const items = data.items;
134
+ const orderTransactionData = {
135
+ orderId: `${order?.id || ''}`,
136
+ transactionTotal: Number(order?.transactionTotal || 0),
137
+ total: Number(order?.total || 0),
138
+ city: order?.city,
139
+ state: order?.state,
140
+ country: order?.country,
141
+ results: items.map((item) => {
142
+ return {
143
+ // uid is required - fallback to get most relevant
144
+ uid: item.uid || item.sku || '',
145
+ childUid: item.childUid,
146
+ sku: item.sku,
147
+ childSku: item.childSku,
148
+ qty: Number(item.qty),
149
+ price: Number(item.price),
150
+ };
151
+ }),
326
152
  };
327
- const event = this.track.event(payload);
328
- if (event) {
329
- // clear cart items from cookie when order is placed
330
- this.cookies.cart.clear();
331
- // legacy tracking
332
- new PixelEvent(payload);
333
- return event;
334
- }
153
+ this.events.order.transaction({ data: orderTransactionData, siteId });
335
154
  },
336
155
  },
337
156
  };
338
- this.updateContext = (key, value) => {
339
- if (value) {
340
- this.context[key] = value;
341
- }
342
- };
343
- this.setCurrency = (currency) => {
344
- if (!currency?.code) {
345
- return;
346
- }
347
- this.context.currency = currency;
348
- };
349
- this.getUserId = () => {
350
- let userId;
351
- try {
352
- // use cookies if available, fallback to localstorage
353
- if (getFlags().cookies()) {
354
- userId = cookies.get(LEGACY_USERID_COOKIE_NAME) || cookies.get(USERID_COOKIE_NAME) || uuidv4();
355
- cookies.set(USERID_COOKIE_NAME, userId, COOKIE_SAMESITE, COOKIE_EXPIRATION, COOKIE_DOMAIN);
356
- cookies.set(LEGACY_USERID_COOKIE_NAME, userId, COOKIE_SAMESITE, COOKIE_EXPIRATION, COOKIE_DOMAIN);
357
- }
358
- else if (getFlags().storage()) {
359
- userId = window.localStorage.getItem(USERID_COOKIE_NAME) || uuidv4();
360
- window.localStorage.setItem(USERID_COOKIE_NAME, userId);
361
- }
362
- else {
363
- throw 'unsupported features';
364
- }
365
- }
366
- catch (e) {
367
- console.error('Failed to persist user id to cookie or local storage:', e);
368
- }
369
- return userId;
370
- };
371
- this.getSessionId = () => {
372
- let sessionId;
373
- if (getFlags().storage()) {
374
- try {
375
- sessionId = window.sessionStorage.getItem(SESSIONID_STORAGE_NAME) || uuidv4();
376
- window.sessionStorage.setItem(SESSIONID_STORAGE_NAME, sessionId);
377
- getFlags().cookies() && cookies.set(SESSIONID_STORAGE_NAME, sessionId, COOKIE_SAMESITE, 0, COOKIE_DOMAIN); //session cookie
378
- }
379
- catch (e) {
380
- console.error('Failed to persist session id to session storage:', e);
381
- }
382
- }
383
- else if (getFlags().cookies()) {
384
- // use cookies if sessionStorage is not enabled and only reset cookie if new session to keep expiration
385
- sessionId = cookies.get(SESSIONID_STORAGE_NAME);
386
- if (!sessionId) {
387
- sessionId = uuidv4();
388
- cookies.set(SESSIONID_STORAGE_NAME, sessionId, COOKIE_SAMESITE, 0, COOKIE_DOMAIN);
389
- }
390
- }
391
- return sessionId;
392
- };
393
- this.getShopperId = () => {
394
- const shopperId = cookies.get(SHOPPERID_COOKIE_NAME);
395
- if (!shopperId) {
396
- return;
397
- }
398
- return shopperId;
399
- };
400
- this.sendPreflight = () => {
401
- const userId = this.getUserId();
402
- const siteId = this.context.website.trackingCode;
403
- const shopper = this.getShopperId();
404
- const cart = this.cookies.cart.get();
405
- const lastViewed = this.cookies.viewed.get();
406
- if (userId && typeof userId == 'string' && siteId && (shopper || cart.length || lastViewed.length)) {
407
- const preflightParams = {
408
- userId,
409
- siteId,
410
- };
411
- let queryStringParams = `?userId=${encodeURIComponent(userId)}&siteId=${encodeURIComponent(siteId)}`;
412
- if (shopper) {
413
- preflightParams.shopper = shopper;
414
- queryStringParams += `&shopper=${encodeURIComponent(shopper)}`;
415
- }
416
- if (cart.length) {
417
- preflightParams.cart = cart;
418
- queryStringParams += cart.map((item) => `&cart=${encodeURIComponent(item)}`).join('');
419
- }
420
- if (lastViewed.length) {
421
- preflightParams.lastViewed = lastViewed;
422
- queryStringParams += lastViewed.map((item) => `&lastViewed=${encodeURIComponent(item)}`).join('');
423
- }
424
- const origin = this.config.requesters?.personalization?.origin || `https://${siteId}.a.searchspring.io`;
425
- const endpoint = `${origin}/api/personalization/preflightCache`;
426
- const xhr = new XMLHttpRequest();
427
- if (charsParams(preflightParams) > 1024) {
428
- xhr.open('POST', endpoint);
429
- xhr.setRequestHeader('Content-Type', 'application/json');
430
- xhr.send(JSON.stringify(preflightParams));
431
- }
432
- else {
433
- xhr.open('GET', endpoint + queryStringParams);
434
- xhr.send();
435
- }
436
- }
437
- };
438
157
  this.cookies = {
439
158
  cart: {
440
159
  get: () => {
441
- const items = cookies.get(CART_PRODUCTS);
442
- if (!items) {
443
- return [];
444
- }
445
- return items.split(',');
160
+ const data = this.storage.cart.get();
161
+ return data.map((item) => this.getProductId(item));
446
162
  },
447
163
  set: (items) => {
448
- if (items.length) {
449
- const cartItems = items.map((item) => `${item}`.trim());
450
- const uniqueCartItems = Array.from(new Set(cartItems));
451
- cookies.set(CART_PRODUCTS, uniqueCartItems.join(','), COOKIE_SAMESITE, 0, COOKIE_DOMAIN);
452
- const itemsHaveChanged = cartItems.filter((item) => items.includes(item)).length !== items.length;
453
- if (itemsHaveChanged) {
454
- this.sendPreflight();
455
- }
456
- }
164
+ const cartItems = items.map((item) => `${item}`.trim());
165
+ const uniqueCartItems = Array.from(new Set(cartItems)).map((uid) => ({ uid, sku: uid, price: 0, qty: 1 }));
166
+ this.storage.cart.set(uniqueCartItems);
457
167
  },
458
168
  add: (items) => {
459
169
  if (items.length) {
460
- const currentCartItems = this.cookies.cart.get();
461
- const itemsToAdd = items.map((item) => `${item}`.trim());
462
- const uniqueCartItems = Array.from(new Set([...currentCartItems, ...itemsToAdd]));
463
- cookies.set(CART_PRODUCTS, uniqueCartItems.join(','), COOKIE_SAMESITE, 0, COOKIE_DOMAIN);
464
- const itemsHaveChanged = currentCartItems.filter((item) => itemsToAdd.includes(item)).length !== itemsToAdd.length;
465
- if (itemsHaveChanged) {
466
- this.sendPreflight();
467
- }
170
+ const itemsToAdd = items.map((item) => `${item}`.trim()).map((uid) => ({ uid, sku: uid, price: 0, qty: 1 }));
171
+ this.storage.cart.add(itemsToAdd);
468
172
  }
469
173
  },
470
174
  remove: (items) => {
471
175
  if (items.length) {
472
- const currentCartItems = this.cookies.cart.get();
473
- const itemsToRemove = items.map((item) => `${item}`.trim());
474
- const updatedItems = currentCartItems.filter((item) => !itemsToRemove.includes(item));
475
- cookies.set(CART_PRODUCTS, updatedItems.join(','), COOKIE_SAMESITE, 0, COOKIE_DOMAIN);
476
- const itemsHaveChanged = currentCartItems.length !== updatedItems.length;
477
- if (itemsHaveChanged) {
478
- this.sendPreflight();
479
- }
176
+ const itemsToRemove = items.map((item) => `${item}`.trim()).map((uid) => ({ uid, sku: uid, price: 0, qty: 1 }));
177
+ this.storage.cart.remove(itemsToRemove);
480
178
  }
481
179
  },
482
180
  clear: () => {
483
- if (this.cookies.cart.get().length) {
484
- cookies.unset(CART_PRODUCTS, COOKIE_DOMAIN);
485
- this.sendPreflight();
486
- }
181
+ this.storage.cart.clear();
487
182
  },
488
183
  },
489
184
  viewed: {
490
185
  get: () => {
491
- const items = cookies.get(VIEWED_PRODUCTS);
492
- if (!items) {
493
- return [];
494
- }
495
- return items.split(',');
186
+ const viewedItems = this.storage.viewed.get();
187
+ return viewedItems.map((item) => this.getProductId(item));
496
188
  },
497
189
  },
498
190
  };
499
- this.sendEvents = (eventsToSend) => {
500
- if (this.mode !== AppMode.production) {
501
- return;
502
- }
503
- const savedEvents = JSON.parse(this.localStorage.get(LOCALSTORAGE_BEACON_POOL_NAME) || '[]');
504
- if (eventsToSend) {
505
- const eventsClone = [];
506
- savedEvents.forEach((_event, idx) => {
507
- // using Object.assign since we are not modifying nested properties
508
- eventsClone.push(Object.assign({}, _event));
509
- delete eventsClone[idx].id;
510
- delete eventsClone[idx].pid;
511
- });
512
- const stringyEventsClone = JSON.stringify(eventsClone);
513
- // de-dupe events
514
- eventsToSend.forEach((event, idx) => {
515
- const newEvent = Object.assign({}, event);
516
- delete newEvent.id;
517
- delete newEvent.pid;
518
- if (stringyEventsClone.indexOf(JSON.stringify(newEvent)) == -1) {
519
- savedEvents.push({ ...eventsToSend[idx] });
520
- }
521
- });
522
- // save the beacon pool with de-duped events
523
- this.localStorage.set(LOCALSTORAGE_BEACON_POOL_NAME, JSON.stringify(savedEvents));
524
- }
525
- clearTimeout(this.isSending);
526
- this.isSending = window.setTimeout(() => {
527
- if (savedEvents.length) {
528
- const xhr = new XMLHttpRequest();
529
- const origin = this.config.requesters?.beacon?.origin || 'https://beacon.searchspring.io';
530
- xhr.open('POST', `${origin}/beacon`);
531
- xhr.setRequestHeader('Content-Type', 'application/json');
532
- xhr.send(JSON.stringify(savedEvents.length == 1 ? savedEvents[0] : savedEvents));
533
- }
534
- this.localStorage.set(LOCALSTORAGE_BEACON_POOL_NAME, JSON.stringify([]));
535
- }, BATCH_TIMEOUT);
536
- };
537
191
  if (typeof globals != 'object' || typeof globals.siteId != 'string') {
538
192
  throw new Error(`Invalid config passed to tracker. The "siteId" attribute must be provided.`);
539
193
  }
540
- this.config = deepmerge(defaultConfig, config || {});
194
+ this.config = config;
541
195
  this.doNotTrack = this.config.doNotTrack || [];
542
196
  if (Object.values(AppMode).includes(this.config.mode)) {
543
197
  this.mode = this.config.mode;
544
198
  }
545
- this.globals = globals;
546
199
  this.localStorage = new StorageStore({
547
200
  type: 'local',
548
201
  key: `ss-${this.config.id}`,
549
202
  });
550
203
  this.localStorage.set('siteId', this.globals.siteId);
551
- this.context = {
552
- userId: this.getUserId() || '',
553
- sessionId: this.getSessionId(),
554
- shopperId: this.getShopperId(),
555
- pageLoadId: uuidv4(),
556
- website: {
557
- trackingCode: this.globals.siteId,
558
- },
559
- };
560
- if (this.globals.currency?.code) {
561
- this.context.currency = this.globals.currency;
562
- }
563
204
  if (!window.searchspring?.tracker) {
564
205
  window.searchspring = window.searchspring || {};
565
206
  window.searchspring.tracker = this;
@@ -660,17 +301,48 @@ export class Tracker {
660
301
  });
661
302
  }
662
303
  });
663
- this.sendEvents();
664
304
  }
665
305
  getGlobals() {
666
306
  return JSON.parse(JSON.stringify(this.globals));
667
307
  }
668
- getContext() {
669
- return JSON.parse(JSON.stringify(this.context));
670
- }
671
308
  retarget() {
672
309
  this.targeters.forEach((target) => {
673
310
  target.retarget();
674
311
  });
675
312
  }
676
313
  }
314
+ function transformToLegacyContext(_context, siteId) {
315
+ const context = { ..._context };
316
+ if (context.userAgent) {
317
+ delete context.userAgent;
318
+ }
319
+ if (context.timestamp) {
320
+ // @ts-ignore - property not optional
321
+ delete context.timestamp;
322
+ }
323
+ if (context.initiator) {
324
+ // @ts-ignore - property not optional
325
+ delete context.initiator;
326
+ }
327
+ if (context.dev) {
328
+ delete context.dev;
329
+ }
330
+ let attribution;
331
+ if (context.attribution?.length) {
332
+ attribution = {
333
+ type: context.attribution[0].type,
334
+ id: context.attribution[0].id,
335
+ };
336
+ delete context.attribution;
337
+ }
338
+ const beaconContext = {
339
+ ...context,
340
+ website: {
341
+ trackingCode: siteId,
342
+ },
343
+ };
344
+ if (attribution) {
345
+ beaconContext.attribution = attribution;
346
+ }
347
+ return beaconContext;
348
+ }