coveo.analytics 2.25.2 → 2.26.1

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.
@@ -70,7 +70,7 @@ var nodePonyfill = {exports: {}};
70
70
 
71
71
  var publicApi = {};
72
72
 
73
- var URL$2 = {exports: {}};
73
+ var URL$3 = {exports: {}};
74
74
 
75
75
  var conversions = {};
76
76
  var lib$1 = conversions;
@@ -79825,9 +79825,9 @@ module.exports = {
79825
79825
  Worker: { URL: URL }
79826
79826
  }
79827
79827
  };
79828
- }(URL$2));
79828
+ }(URL$3));
79829
79829
 
79830
- publicApi.URL = URL$2.exports.interface;
79830
+ publicApi.URL = URL$3.exports.interface;
79831
79831
  publicApi.serializeURL = urlStateMachine.exports.serializeURL;
79832
79832
  publicApi.serializeURLOrigin = urlStateMachine.exports.serializeURLOrigin;
79833
79833
  publicApi.basicURLParse = urlStateMachine.exports.basicURLParse;
@@ -80968,7 +80968,7 @@ Object.defineProperty(Response.prototype, Symbol.toStringTag, {
80968
80968
  });
80969
80969
 
80970
80970
  const INTERNALS$2 = Symbol('Request internals');
80971
- const URL$1 = Url.URL || publicApi.URL;
80971
+ const URL$2 = Url.URL || publicApi.URL;
80972
80972
 
80973
80973
  // fix an issue where "format", "parse" aren't a named export for node <10
80974
80974
  const parse_url = Url.parse;
@@ -80987,7 +80987,7 @@ function parseURL(urlStr) {
80987
80987
  Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
80988
80988
  */
80989
80989
  if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlStr)) {
80990
- urlStr = new URL$1(urlStr).toString();
80990
+ urlStr = new URL$2(urlStr).toString();
80991
80991
  }
80992
80992
 
80993
80993
  // Fallback to old implementation for arbitrary URLs
@@ -81696,7 +81696,7 @@ function stringToBytes(str) {
81696
81696
  }
81697
81697
 
81698
81698
  const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
81699
- const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
81699
+ const URL$1 = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
81700
81700
  function v35(name, version, hashfunc) {
81701
81701
  function generateUUID(value, namespace, buf, offset) {
81702
81702
  var _namespace;
@@ -81743,7 +81743,7 @@ function v35(name, version, hashfunc) {
81743
81743
 
81744
81744
 
81745
81745
  generateUUID.DNS = DNS;
81746
- generateUUID.URL = URL;
81746
+ generateUUID.URL = URL$1;
81747
81747
  return generateUUID;
81748
81748
  }
81749
81749
 
@@ -82158,7 +82158,197 @@ const addPageViewToHistory = (pageViewValue) => __awaiter(void 0, void 0, void 0
82158
82158
  yield store.addElementAsync(historyElement);
82159
82159
  });
82160
82160
 
82161
- const libVersion = "2.25.2" ;
82161
+ const libVersion = "2.26.1" ;
82162
+
82163
+ const getFormattedLocation = (location) => `${location.protocol}//${location.hostname}${location.pathname.indexOf('/') === 0 ? location.pathname : `/${location.pathname}`}${location.search}`;
82164
+
82165
+ const BasePluginEventTypes = {
82166
+ pageview: 'pageview',
82167
+ event: 'event',
82168
+ };
82169
+ class Plugin {
82170
+ constructor({ client, uuidGenerator = v4 }) {
82171
+ this.client = client;
82172
+ this.uuidGenerator = uuidGenerator;
82173
+ }
82174
+ }
82175
+ class BasePlugin extends Plugin {
82176
+ constructor({ client, uuidGenerator = v4 }) {
82177
+ super({ client, uuidGenerator });
82178
+ this.actionData = {};
82179
+ this.pageViewId = uuidGenerator();
82180
+ this.nextPageViewId = this.pageViewId;
82181
+ this.currentLocation = getFormattedLocation(window.location);
82182
+ this.lastReferrer = hasDocument() ? document.referrer : '';
82183
+ this.addHooks();
82184
+ }
82185
+ getApi(name) {
82186
+ switch (name) {
82187
+ case 'setAction':
82188
+ return this.setAction;
82189
+ default:
82190
+ return null;
82191
+ }
82192
+ }
82193
+ setAction(action, options) {
82194
+ this.action = action;
82195
+ this.actionData = options;
82196
+ }
82197
+ clearData() {
82198
+ this.clearPluginData();
82199
+ this.action = undefined;
82200
+ this.actionData = {};
82201
+ }
82202
+ getLocationInformation(eventType, payload) {
82203
+ return Object.assign({ hitType: eventType }, this.getNextValues(eventType, payload));
82204
+ }
82205
+ updateLocationInformation(eventType, payload) {
82206
+ this.updateLocationForNextPageView(eventType, payload);
82207
+ }
82208
+ getDefaultContextInformation(eventType) {
82209
+ const documentContext = {
82210
+ title: hasDocument() ? document.title : '',
82211
+ encoding: hasDocument() ? document.characterSet : 'UTF-8',
82212
+ };
82213
+ const screenContext = {
82214
+ screenResolution: `${screen.width}x${screen.height}`,
82215
+ screenColor: `${screen.colorDepth}-bit`,
82216
+ };
82217
+ const navigatorContext = {
82218
+ language: navigator.language,
82219
+ userAgent: navigator.userAgent,
82220
+ };
82221
+ const eventContext = {
82222
+ time: Date.now(),
82223
+ eventId: this.uuidGenerator(),
82224
+ };
82225
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, eventContext), screenContext), navigatorContext), documentContext);
82226
+ }
82227
+ updateLocationForNextPageView(eventType, payload) {
82228
+ const { pageViewId, referrer, location } = this.getNextValues(eventType, payload);
82229
+ this.lastReferrer = referrer;
82230
+ this.pageViewId = pageViewId;
82231
+ this.currentLocation = location;
82232
+ if (eventType === BasePluginEventTypes.pageview) {
82233
+ this.nextPageViewId = this.uuidGenerator();
82234
+ this.hasSentFirstPageView = true;
82235
+ }
82236
+ }
82237
+ getNextValues(eventType, payload) {
82238
+ return {
82239
+ pageViewId: eventType === BasePluginEventTypes.pageview ? this.nextPageViewId : this.pageViewId,
82240
+ referrer: eventType === BasePluginEventTypes.pageview && this.hasSentFirstPageView
82241
+ ? this.currentLocation
82242
+ : this.lastReferrer,
82243
+ location: eventType === BasePluginEventTypes.pageview
82244
+ ? this.getCurrentLocationFromPayload(payload)
82245
+ : this.currentLocation,
82246
+ };
82247
+ }
82248
+ getCurrentLocationFromPayload(payload) {
82249
+ if (!!payload.page) {
82250
+ const removeStartingSlash = (page) => page.replace(/^\/?(.*)$/, '/$1');
82251
+ const extractHostnamePart = (location) => location.split('/').slice(0, 3).join('/');
82252
+ return `${extractHostnamePart(this.currentLocation)}${removeStartingSlash(payload.page)}`;
82253
+ }
82254
+ else {
82255
+ return getFormattedLocation(window.location);
82256
+ }
82257
+ }
82258
+ }
82259
+
82260
+ class CoveoLinkParam {
82261
+ constructor(clientId, timestamp) {
82262
+ if (!validate(clientId))
82263
+ throw Error('Not a valid uuid');
82264
+ this.clientId = clientId;
82265
+ this.creationDate = Math.floor(timestamp / 1000);
82266
+ }
82267
+ toString() {
82268
+ return this.clientId.replace(/-/g, '') + '.' + this.creationDate.toString();
82269
+ }
82270
+ get expired() {
82271
+ const age = Math.floor(Date.now() / 1000) - this.creationDate;
82272
+ return age < 0 || age > CoveoLinkParam.expirationTime;
82273
+ }
82274
+ validate(referrerString, referrerList) {
82275
+ return !this.expired && this.matchReferrer(referrerString, referrerList);
82276
+ }
82277
+ matchReferrer(referrerString, referrerList) {
82278
+ try {
82279
+ const url = new URL(referrerString);
82280
+ return referrerList.some((value) => {
82281
+ const hostRegExp = new RegExp(value.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
82282
+ return hostRegExp.test(url.host);
82283
+ });
82284
+ }
82285
+ catch (error) {
82286
+ return false;
82287
+ }
82288
+ }
82289
+ static fromString(input) {
82290
+ const parts = input.split('.');
82291
+ if (parts.length !== 2) {
82292
+ return null;
82293
+ }
82294
+ const [clientIdPart, creationDate] = parts;
82295
+ if (clientIdPart.length !== 32 || isNaN(parseInt(creationDate))) {
82296
+ return null;
82297
+ }
82298
+ const clientId = clientIdPart.substring(0, 8) +
82299
+ '-' +
82300
+ clientIdPart.substring(8, 12) +
82301
+ '-' +
82302
+ clientIdPart.substring(12, 16) +
82303
+ '-' +
82304
+ clientIdPart.substring(16, 20) +
82305
+ '-' +
82306
+ clientIdPart.substring(20, 32);
82307
+ if (validate(clientId)) {
82308
+ return new CoveoLinkParam(clientId, Number.parseInt(creationDate) * 1000);
82309
+ }
82310
+ else {
82311
+ return null;
82312
+ }
82313
+ }
82314
+ }
82315
+ CoveoLinkParam.cvo_cid = 'cvo_cid';
82316
+ CoveoLinkParam.expirationTime = 120;
82317
+ class LinkPlugin extends Plugin {
82318
+ constructor({ client, uuidGenerator = v4 }) {
82319
+ super({ client, uuidGenerator });
82320
+ }
82321
+ getApi(name) {
82322
+ switch (name) {
82323
+ case 'decorate':
82324
+ return this.decorate;
82325
+ case 'acceptFrom':
82326
+ return this.acceptFrom;
82327
+ default:
82328
+ return null;
82329
+ }
82330
+ }
82331
+ decorate(urlString) {
82332
+ return __awaiter(this, void 0, void 0, function* () {
82333
+ if (!this.client.getCurrentVisitorId) {
82334
+ throw new Error('Could not retrieve current clientId');
82335
+ }
82336
+ try {
82337
+ const url = new URL(urlString);
82338
+ const clientId = yield this.client.getCurrentVisitorId();
82339
+ url.searchParams.set(CoveoLinkParam.cvo_cid, new CoveoLinkParam(clientId, Date.now()).toString());
82340
+ return url.toString();
82341
+ }
82342
+ catch (error) {
82343
+ throw new Error('Invalid URL provided');
82344
+ }
82345
+ });
82346
+ }
82347
+ acceptFrom(acceptedReferrers) {
82348
+ this.client.setAcceptedLinkReferrers(acceptedReferrers);
82349
+ }
82350
+ }
82351
+ LinkPlugin.Id = 'link';
82162
82352
 
82163
82353
  const keysOf = Object.keys;
82164
82354
  function isObject(o) {
@@ -82522,6 +82712,7 @@ function buildBaseUrl(endpoint = Endpoints.default, apiVersion = Version) {
82522
82712
  const COVEO_NAMESPACE = '38824e1f-37f5-42d3-8372-a4b8fa9df946';
82523
82713
  class CoveoAnalyticsClient {
82524
82714
  constructor(opts) {
82715
+ this.acceptedLinkReferrers = [];
82525
82716
  if (!opts) {
82526
82717
  throw new Error('You have to pass options to this constructor');
82527
82718
  }
@@ -82574,7 +82765,9 @@ class CoveoAnalyticsClient {
82574
82765
  determineVisitorId() {
82575
82766
  return __awaiter(this, void 0, void 0, function* () {
82576
82767
  try {
82577
- return (yield this.storage.getItem('visitorId')) || v4();
82768
+ return (this.extractClientIdFromLink(window.location.href) ||
82769
+ (yield this.storage.getItem('visitorId')) ||
82770
+ v4());
82578
82771
  }
82579
82772
  catch (err) {
82580
82773
  console.log('Could not get visitor ID from the current runtime environment storage. Using a random ID instead.', err);
@@ -82632,6 +82825,25 @@ class CoveoAnalyticsClient {
82632
82825
  this.visitorId = visitorId;
82633
82826
  this.storage.setItem('visitorId', visitorId);
82634
82827
  }
82828
+ extractClientIdFromLink(urlString) {
82829
+ if (doNotTrack()) {
82830
+ return null;
82831
+ }
82832
+ try {
82833
+ const linkParam = new URL(urlString).searchParams.get(CoveoLinkParam.cvo_cid);
82834
+ if (linkParam == null) {
82835
+ return null;
82836
+ }
82837
+ const linker = CoveoLinkParam.fromString(linkParam);
82838
+ if (!linker || !hasDocument() || !linker.validate(document.referrer, this.acceptedLinkReferrers)) {
82839
+ return null;
82840
+ }
82841
+ return linker.clientId;
82842
+ }
82843
+ catch (error) {
82844
+ }
82845
+ return null;
82846
+ }
82635
82847
  resolveParameters(eventType, ...payload) {
82636
82848
  return __awaiter(this, void 0, void 0, function* () {
82637
82849
  const { variableLengthArgumentsNames = [], addVisitorIdParameter = false, usesMeasurementProtocol = false, addClientIdParameter = false, } = this.eventTypeMapping[eventType] || {};
@@ -82799,6 +83011,12 @@ class CoveoAnalyticsClient {
82799
83011
  addEventTypeMapping(eventType, eventConfig) {
82800
83012
  this.eventTypeMapping[eventType] = eventConfig;
82801
83013
  }
83014
+ setAcceptedLinkReferrers(hosts) {
83015
+ if (Array.isArray(hosts) && hosts.every((host) => typeof host == 'string'))
83016
+ this.acceptedLinkReferrers = hosts;
83017
+ else
83018
+ throw Error('Parameter should be an array of domain strings');
83019
+ }
82802
83020
  parseVariableArgumentsPayload(fieldsOrder, payload) {
82803
83021
  const parsedArguments = {};
82804
83022
  for (let i = 0, length = payload.length; i < length; i++) {
@@ -83833,90 +84051,6 @@ class CoveoSearchPageClient {
83833
84051
  }
83834
84052
  }
83835
84053
 
83836
- const getFormattedLocation = (location) => `${location.protocol}//${location.hostname}${location.pathname.indexOf('/') === 0 ? location.pathname : `/${location.pathname}`}${location.search}`;
83837
-
83838
- const BasePluginEventTypes = {
83839
- pageview: 'pageview',
83840
- event: 'event',
83841
- };
83842
- class BasePlugin {
83843
- constructor({ client, uuidGenerator = v4 }) {
83844
- this.actionData = {};
83845
- this.client = client;
83846
- this.uuidGenerator = uuidGenerator;
83847
- this.pageViewId = uuidGenerator();
83848
- this.nextPageViewId = this.pageViewId;
83849
- this.currentLocation = getFormattedLocation(window.location);
83850
- this.lastReferrer = hasDocument() ? document.referrer : '';
83851
- this.addHooks();
83852
- }
83853
- setAction(action, options) {
83854
- this.action = action;
83855
- this.actionData = options;
83856
- }
83857
- clearData() {
83858
- this.clearPluginData();
83859
- this.action = undefined;
83860
- this.actionData = {};
83861
- }
83862
- getLocationInformation(eventType, payload) {
83863
- return Object.assign({ hitType: eventType }, this.getNextValues(eventType, payload));
83864
- }
83865
- updateLocationInformation(eventType, payload) {
83866
- this.updateLocationForNextPageView(eventType, payload);
83867
- }
83868
- getDefaultContextInformation(eventType) {
83869
- const documentContext = {
83870
- title: hasDocument() ? document.title : '',
83871
- encoding: hasDocument() ? document.characterSet : 'UTF-8',
83872
- };
83873
- const screenContext = {
83874
- screenResolution: `${screen.width}x${screen.height}`,
83875
- screenColor: `${screen.colorDepth}-bit`,
83876
- };
83877
- const navigatorContext = {
83878
- language: navigator.language,
83879
- userAgent: navigator.userAgent,
83880
- };
83881
- const eventContext = {
83882
- time: Date.now(),
83883
- eventId: this.uuidGenerator(),
83884
- };
83885
- return Object.assign(Object.assign(Object.assign(Object.assign({}, eventContext), screenContext), navigatorContext), documentContext);
83886
- }
83887
- updateLocationForNextPageView(eventType, payload) {
83888
- const { pageViewId, referrer, location } = this.getNextValues(eventType, payload);
83889
- this.lastReferrer = referrer;
83890
- this.pageViewId = pageViewId;
83891
- this.currentLocation = location;
83892
- if (eventType === BasePluginEventTypes.pageview) {
83893
- this.nextPageViewId = this.uuidGenerator();
83894
- this.hasSentFirstPageView = true;
83895
- }
83896
- }
83897
- getNextValues(eventType, payload) {
83898
- return {
83899
- pageViewId: eventType === BasePluginEventTypes.pageview ? this.nextPageViewId : this.pageViewId,
83900
- referrer: eventType === BasePluginEventTypes.pageview && this.hasSentFirstPageView
83901
- ? this.currentLocation
83902
- : this.lastReferrer,
83903
- location: eventType === BasePluginEventTypes.pageview
83904
- ? this.getCurrentLocationFromPayload(payload)
83905
- : this.currentLocation,
83906
- };
83907
- }
83908
- getCurrentLocationFromPayload(payload) {
83909
- if (!!payload.page) {
83910
- const removeStartingSlash = (page) => page.replace(/^\/?(.*)$/, '/$1');
83911
- const extractHostnamePart = (location) => location.split('/').slice(0, 3).join('/');
83912
- return `${extractHostnamePart(this.currentLocation)}${removeStartingSlash(payload.page)}`;
83913
- }
83914
- else {
83915
- return getFormattedLocation(window.location);
83916
- }
83917
- }
83918
- }
83919
-
83920
84054
  const SVCPluginEventTypes = Object.assign({}, BasePluginEventTypes);
83921
84055
  const allSVCEventTypes = Object.keys(SVCPluginEventTypes).map((key) => SVCPluginEventTypes[key]);
83922
84056
  class SVCPlugin extends BasePlugin {
@@ -83924,6 +84058,17 @@ class SVCPlugin extends BasePlugin {
83924
84058
  super({ client, uuidGenerator });
83925
84059
  this.ticket = {};
83926
84060
  }
84061
+ getApi(name) {
84062
+ const superCall = super.getApi(name);
84063
+ if (superCall !== null)
84064
+ return superCall;
84065
+ switch (name) {
84066
+ case 'setTicket':
84067
+ return this.setTicket;
84068
+ default:
84069
+ return null;
84070
+ }
84071
+ }
83927
84072
  addHooks() {
83928
84073
  this.addHooksForEvent();
83929
84074
  this.addHooksForPageView();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coveo.analytics",
3
- "version": "2.25.2",
3
+ "version": "2.26.1",
4
4
  "description": "📈 Coveo analytics client (node and browser compatible) ",
5
5
  "main": "dist/library.js",
6
6
  "module": "dist/library.es.js",
@@ -7,6 +7,8 @@ import {mockFetch} from '../../tests/fetchMock';
7
7
  import {BrowserRuntime} from './runtimeEnvironment';
8
8
  import * as doNotTrack from '../donottrack';
9
9
  import {Cookie} from '../cookieutils';
10
+ import {v4 as uuidv4} from 'uuid';
11
+ import {CoveoLinkParam} from '../plugins/link';
10
12
 
11
13
  const aVisitorId = '123';
12
14
  jest.mock('uuid', () => ({
@@ -483,3 +485,123 @@ describe('custom clientId', () => {
483
485
  //uuid v5 specific uuid generation
484
486
  });
485
487
  });
488
+
489
+ describe('clientId from link', () => {
490
+ // note: referrer is set as http://somewhere.over/thereferrer in setup.js
491
+ let client: CoveoAnalyticsClient;
492
+ const forcedUUID: string = 'c0b48880-743e-484f-8044-d7c37910c55b';
493
+
494
+ function navigateTo(url: string) {
495
+ // @ts-ignore
496
+ delete window.location;
497
+ // @ts-ignore
498
+ window.location = new URL(url);
499
+ }
500
+
501
+ beforeEach(() => {
502
+ client = new CoveoAnalyticsClient({});
503
+ // need to clear existing clientIds
504
+ client.clear();
505
+ jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => false);
506
+ });
507
+
508
+ it('will extract a clientId from a query param if the referrer matches all and it is not expired', async () => {
509
+ client.setAcceptedLinkReferrers(['*']);
510
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
511
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
512
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
513
+ });
514
+
515
+ it('will extract a clientId from a query param if the referrer matches the current referrer exactly and it is not expired', async () => {
516
+ client.setAcceptedLinkReferrers(['somewhere.over']);
517
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
518
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
519
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
520
+ });
521
+
522
+ it('will extract a clientId from a query param if the referrer matches the current referrer with wildcard and it is not expired', async () => {
523
+ client.setAcceptedLinkReferrers(['*.over']);
524
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
525
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
526
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
527
+ });
528
+
529
+ it('will extract a clientId from a query param if one of the referrer matches the current referrer and it is not expired', async () => {
530
+ client.setAcceptedLinkReferrers(['*.mydomain.com', '*.over']);
531
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
532
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
533
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
534
+ });
535
+
536
+ it('will not extract a clientId from a query param if the referrer matches and it is expired', async () => {
537
+ client.setAcceptedLinkReferrers(['*']);
538
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now() - 180000);
539
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
540
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
541
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
542
+ });
543
+
544
+ it('will not extract a clientId from a query param if there is no accept list specified', async () => {
545
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
546
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
547
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
548
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
549
+ });
550
+
551
+ it('will not extract a clientId from a query param if there is an empty accept list', async () => {
552
+ client.setAcceptedLinkReferrers([]);
553
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
554
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
555
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
556
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
557
+ });
558
+
559
+ it('will not extract a clientId from a query param if the referrer list does not match', async () => {
560
+ client.setAcceptedLinkReferrers(['*.mydomain.com']);
561
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
562
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
563
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
564
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
565
+ });
566
+
567
+ it('will not extract a clientId from a query param if the referrer list does not match the exact port', async () => {
568
+ client.setAcceptedLinkReferrers(['*.over:9000']);
569
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
570
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
571
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
572
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
573
+ });
574
+
575
+ it('will not extract a clientId from a query param if the multi referrer list does not match', async () => {
576
+ client.setAcceptedLinkReferrers(['*.mydomain.com', 'www.example.com']);
577
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
578
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
579
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
580
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
581
+ });
582
+
583
+ it('will not extract a clientId from a query param if it is not a UUID', async () => {
584
+ client.setAcceptedLinkReferrers(['*']);
585
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=notauuid.' + Math.floor(Date.now() / 1000));
586
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
587
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
588
+ });
589
+
590
+ it('will not extract a clientId from a query param if DNT is enabled', async () => {
591
+ client.setAcceptedLinkReferrers(['*']);
592
+ jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => true);
593
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
594
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
595
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
596
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
597
+ });
598
+
599
+ it('will throw when specifying invalid hosts list', async () => {
600
+ //@ts-ignore
601
+ expect(() => client.setAcceptedLinkReferrers('*')).toThrow('Parameter should be an array of domain strings');
602
+ //@ts-ignore
603
+ expect(() => client.setAcceptedLinkReferrers({})).toThrow('Parameter should be an array of domain strings');
604
+ //@ts-ignore
605
+ expect(() => client.setAcceptedLinkReferrers([{}])).toThrow('Parameter should be an array of domain strings');
606
+ });
607
+ });
@@ -24,6 +24,7 @@ import {addDefaultValues} from '../hook/addDefaultValues';
24
24
  import {enhanceViewEvent} from '../hook/enhanceViewEvent';
25
25
  import {v4 as uuidv4, v5 as uuidv5, validate as uuidValidate} from 'uuid';
26
26
  import {libVersion} from '../version';
27
+ import {CoveoLinkParam} from '../plugins/link';
27
28
  import {
28
29
  convertKeysToMeasurementProtocol,
29
30
  isMeasurementProtocolKey,
@@ -105,6 +106,7 @@ export interface AnalyticsClient {
105
106
  */
106
107
  readonly currentVisitorId: string;
107
108
  getCurrentVisitorId?(): Promise<string>; // TODO: v3 make required
109
+ setAcceptedLinkReferrers?(hosts: string[]): void;
108
110
  }
109
111
 
110
112
  export interface BufferedRequest {
@@ -146,6 +148,7 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
146
148
  private afterSendHooks: AnalyticsClientSendEventHook[];
147
149
  private eventTypeMapping: {[name: string]: EventTypeConfig};
148
150
  private options: ClientOptions;
151
+ private acceptedLinkReferrers: string[] = [];
149
152
 
150
153
  constructor(opts: Partial<ClientOptions>) {
151
154
  if (!opts) {
@@ -196,7 +199,11 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
196
199
 
197
200
  private async determineVisitorId() {
198
201
  try {
199
- return (await this.storage.getItem('visitorId')) || uuidv4();
202
+ return (
203
+ this.extractClientIdFromLink(window.location.href) ||
204
+ (await this.storage.getItem('visitorId')) ||
205
+ uuidv4()
206
+ );
200
207
  } catch (err) {
201
208
  console.log(
202
209
  'Could not get visitor ID from the current runtime environment storage. Using a random ID instead.',
@@ -258,6 +265,26 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
258
265
  this.storage.setItem('visitorId', visitorId);
259
266
  }
260
267
 
268
+ private extractClientIdFromLink(urlString: string): string | null {
269
+ if (doNotTrack()) {
270
+ return null;
271
+ }
272
+ try {
273
+ const linkParam: string | null = new URL(urlString).searchParams.get(CoveoLinkParam.cvo_cid);
274
+ if (linkParam == null) {
275
+ return null;
276
+ }
277
+ const linker: CoveoLinkParam | null = CoveoLinkParam.fromString(linkParam);
278
+ if (!linker || !hasDocument() || !linker.validate(document.referrer, this.acceptedLinkReferrers)) {
279
+ return null;
280
+ }
281
+ return linker.clientId;
282
+ } catch (error) {
283
+ // Ignore any parsing errors
284
+ }
285
+ return null;
286
+ }
287
+
261
288
  async resolveParameters(eventType: EventType | string, ...payload: VariableArgumentsPayload) {
262
289
  const {
263
290
  variableLengthArgumentsNames = [],
@@ -453,6 +480,11 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
453
480
  this.eventTypeMapping[eventType] = eventConfig;
454
481
  }
455
482
 
483
+ setAcceptedLinkReferrers(hosts: string[]): void {
484
+ if (Array.isArray(hosts) && hosts.every((host) => typeof host == 'string')) this.acceptedLinkReferrers = hosts;
485
+ else throw Error('Parameter should be an array of domain strings');
486
+ }
487
+
456
488
  private parseVariableArgumentsPayload(fieldsOrder: string[], payload: VariableArgumentsPayload) {
457
489
  const parsedArguments: {[name: string]: any} = {};
458
490
  for (let i = 0, length = payload.length; i < length; i++) {
@@ -1,15 +1,14 @@
1
- import {PluginClass, PluginOptions, BasePlugin} from '../plugins/BasePlugin';
1
+ import {PluginClass, PluginOptions, BasePlugin, Plugin} from '../plugins/BasePlugin';
2
2
  import {EC} from '../plugins/ec';
3
+ import {Link} from '../plugins/link';
3
4
  import {SVC} from '../plugins/svc';
4
5
 
5
- export type UAPluginOptions = any[];
6
- export type Plugin = BasePlugin & {[propName: string]: unknown};
7
-
8
6
  export class Plugins {
9
- public static readonly DefaultPlugins: string[] = [EC.Id, SVC.Id];
7
+ public static readonly DefaultPlugins: string[] = [EC.Id, SVC.Id, Link.Id];
10
8
  private registeredPluginsMap: Record<string, PluginClass> = {
11
9
  [EC.Id]: EC,
12
10
  [SVC.Id]: SVC,
11
+ [Link.Id]: Link,
13
12
  };
14
13
  private requiredPlugins: Record<string, BasePlugin> = {};
15
14
 
@@ -31,18 +30,18 @@ export class Plugins {
31
30
  this.requiredPlugins = {};
32
31
  }
33
32
 
34
- execute(name: string, fn: string, ...pluginOptions: UAPluginOptions) {
33
+ execute(name: string, fn: string, ...args: any[]): any {
35
34
  const plugin = this.requiredPlugins[name] as Plugin;
36
35
  if (!plugin) {
37
36
  throw new Error(`The plugin "${name}" is not required. Check that you required it on initialization.`);
38
37
  }
39
- const actionFunction = plugin[fn];
38
+ const actionFunction = plugin.getApi(fn);
40
39
  if (!actionFunction) {
41
40
  throw new Error(`The function "${fn}" does not exist on the plugin "${name}".`);
42
41
  }
43
42
  if (typeof actionFunction !== 'function') {
44
43
  throw new Error(`"${fn}" of the plugin "${name}" is not a function.`);
45
44
  }
46
- return actionFunction.apply(plugin, pluginOptions);
45
+ return actionFunction.apply(plugin, args);
47
46
  }
48
47
  }