coveo.analytics 2.23.10 → 2.24.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coveo.analytics",
3
- "version": "2.23.10",
3
+ "version": "2.24.1",
4
4
  "description": "📈 Coveo analytics client (node and browser compatible) ",
5
5
  "main": "dist/library.js",
6
6
  "module": "dist/library.es.js",
@@ -23,18 +23,21 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "cross-fetch": "^3.1.5"
26
+ "cross-fetch": "^3.1.5",
27
+ "uuid": "^9.0.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@rollup/plugin-alias": "^3.1.2",
30
31
  "@rollup/plugin-commonjs": "^19.0.0",
31
32
  "@rollup/plugin-json": "^4.1.0",
32
33
  "@rollup/plugin-node-resolve": "^13.0.0",
34
+ "@rollup/plugin-replace": "^5.0.2",
33
35
  "@types/fetch-mock": "^7.3.2",
34
36
  "@types/jest": "^25.1.1",
35
37
  "@types/jsdom": "^12.2.4",
36
38
  "@types/mime": "0.0.29",
37
39
  "@types/node": "^6.0.45",
40
+ "@types/uuid": "^9.0.0",
38
41
  "babel-core": "^6.17.0",
39
42
  "babel-loader": "^7.1.4",
40
43
  "babel-preset-es2015": "^6.16.0",
@@ -66,7 +66,7 @@ describe('Analytics', () => {
66
66
 
67
67
  expect(fetchMock.called()).toBe(true);
68
68
 
69
- const [path, {headers, body}] = fetchMock.lastCall();
69
+ const [path, {headers, body}]: any = fetchMock.lastCall();
70
70
  expect(path).toBe(endpointForEventType(EventType.view));
71
71
 
72
72
  const h = headers as Record<string, string>;
@@ -115,8 +115,8 @@ describe('Analytics', () => {
115
115
 
116
116
  await Promise.all([firstRequest, secondRequest, thirdRequest]);
117
117
 
118
- const assertResponseHasCustomDataWithIndex = (response: RequestInit, index: number) => {
119
- const parsedBody = JSON.parse(response.body.toString());
118
+ const assertResponseHasCustomDataWithIndex = (response: RequestInit | undefined, index: number) => {
119
+ const parsedBody = JSON.parse(response!.body!.toString());
120
120
  expect(parsedBody.customData.index).toBe(index);
121
121
  };
122
122
  const [[, firstResponse], [, secondResponse], [, thirdResponse]] = fetchMock.calls();
@@ -170,7 +170,7 @@ describe('Analytics', () => {
170
170
  it('should properly parse empty arguments', async () => {
171
171
  await client.sendEvent(specialEventType);
172
172
 
173
- const [path, {body}] = fetchMock.lastCall();
173
+ const [path, {body}]: any = fetchMock.lastCall();
174
174
  expect(path).toBe(endpointForEventType(EventType.custom));
175
175
 
176
176
  const parsedBody = JSON.parse(body.toString());
@@ -409,16 +409,16 @@ describe('Analytics', () => {
409
409
 
410
410
  const call = fetchMock.calls()[0];
411
411
  const url = call[0];
412
- const options: RequestInit = call[1];
412
+ const options: RequestInit | undefined = call[1];
413
413
 
414
414
  const {url: expectedUrl, ...expectedOptions} = processedRequest;
415
- expect(clientOrigin).toBe('analyticsFetch');
415
+ expect(clientOrigin!).toBe('analyticsFetch');
416
416
  expect(url).toBe(expectedUrl);
417
417
  expect(options).toEqual(expectedOptions);
418
418
  });
419
419
 
420
420
  const getParsedBodyCalls = (): any[] => {
421
- return fetchMock.calls().map(([, {body}]) => {
421
+ return fetchMock.calls().map(([, {body}]: any) => {
422
422
  return JSON.parse(body.toString());
423
423
  });
424
424
  };
@@ -453,3 +453,34 @@ describe('doNotTrack', () => {
453
453
  expect(Cookie.get('coveo_visitorId')).not.toBe(aVisitorId);
454
454
  });
455
455
  });
456
+
457
+ describe('custom clientId', () => {
458
+ it('allows setting of a custom clientId', async () => {
459
+ const client = new CoveoAnalyticsClient({});
460
+ client.setClientId('c7d57b22-4aa8-487a-a106-be5243885f0a');
461
+ expect(await client.getCurrentVisitorId()).toBe('c7d57b22-4aa8-487a-a106-be5243885f0a');
462
+ });
463
+
464
+ it('allows setting a custom consistent clientId given a string', async () => {
465
+ const client = new CoveoAnalyticsClient({});
466
+ client.setClientId('somestring', 'testNameSpace');
467
+ //uuid v5 specific uuid generation
468
+ expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
469
+ //check for consistent ids
470
+ client.setClientId('somestring', 'testNameSpace');
471
+ expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
472
+ client.setClientId('otherstring', 'testNameSpace');
473
+ expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
474
+ client.setClientId('somestring', 'otherNameSpace');
475
+ expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
476
+ });
477
+
478
+ it('errors when not providing a namespace', async () => {
479
+ const client = new CoveoAnalyticsClient({});
480
+ expect.assertions(1);
481
+ await expect(client.setClientId('somestring')).rejects.toEqual(
482
+ Error('Cannot generate uuid client id without a specific namespace string.')
483
+ );
484
+ //uuid v5 specific uuid generation
485
+ });
486
+ });
@@ -23,6 +23,8 @@ import {hasWindow, hasDocument} from '../detector';
23
23
  import {addDefaultValues} from '../hook/addDefaultValues';
24
24
  import {enhanceViewEvent} from '../hook/enhanceViewEvent';
25
25
  import {uuidv4} from './crypto';
26
+ import {v5 as uuidv5, validate as uuidValidate} from 'uuid';
27
+ import {libVersion} from '../version';
26
28
  import {
27
29
  convertKeysToMeasurementProtocol,
28
30
  isMeasurementProtocolKey,
@@ -98,6 +100,7 @@ export interface AnalyticsClient {
98
100
  registerAfterSendEventHook(hook: AnalyticsClientSendEventHook): void;
99
101
  addEventTypeMapping(eventType: string, eventConfig: EventTypeConfig): void;
100
102
  runtime: IRuntimeEnvironment;
103
+ version: string;
101
104
  /**
102
105
  * @deprecated
103
106
  */
@@ -118,6 +121,11 @@ export function buildBaseUrl(endpoint = Endpoints.default, apiVersion = Version)
118
121
  return `${endpoint}${endpointIsCoveoProxy ? '' : '/rest'}/${apiVersion}`;
119
122
  }
120
123
 
124
+ // Note: Changing this value will destroy the mapping from tracking string to clientId for all customers
125
+ // using the setClientId() api. It will have the same effect as every visitor for those customers clearing
126
+ // their cookie store at the same time, with corresponding downstream effects.
127
+ const COVEO_NAMESPACE = '38824e1f-37f5-42d3-8372-a4b8fa9df946';
128
+
121
129
  export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider {
122
130
  private get defaultOptions(): ClientOptions {
123
131
  return {
@@ -130,6 +138,9 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
130
138
  }
131
139
 
132
140
  public runtime: IRuntimeEnvironment;
141
+ public get version(): string {
142
+ return libVersion;
143
+ }
133
144
  private visitorId: string;
134
145
  private bufferedRequests: BufferedRequest[];
135
146
  private beforeSendHooks: AnalyticsClientSendEventHook[];
@@ -209,6 +220,17 @@ export class CoveoAnalyticsClient implements AnalyticsClient, VisitorIdProvider
209
220
  await this.storage.setItem('visitorId', visitorId);
210
221
  }
211
222
 
223
+ async setClientId(value: string, namespace?: string) {
224
+ if (uuidValidate(value)) {
225
+ this.setCurrentVisitorId(value);
226
+ } else {
227
+ if (!namespace) {
228
+ throw Error('Cannot generate uuid client id without a specific namespace string.');
229
+ }
230
+ this.setCurrentVisitorId(uuidv5(value, uuidv5(namespace, COVEO_NAMESPACE)));
231
+ }
232
+ }
233
+
212
234
  async getParameters(eventType: EventType | string, ...payload: VariableArgumentsPayload) {
213
235
  return await this.resolveParameters(eventType, ...payload);
214
236
  }
@@ -124,7 +124,7 @@ describe('AnalyticsBeaconClient', () => {
124
124
 
125
125
  await client.sendEvent(EventType.collect, {});
126
126
 
127
- expect(clientOrigin).toBe('analyticsBeacon');
127
+ expect(clientOrigin!).toBe('analyticsBeacon');
128
128
  expect(sendBeaconMock).toHaveBeenCalledWith(processedRequest.url, processedRequest.body);
129
129
  });
130
130
 
@@ -17,6 +17,7 @@ import {
17
17
  PreparedViewEventRequest,
18
18
  ViewEventRequest,
19
19
  } from '../events';
20
+ import {libVersion} from '../version';
20
21
  import {NoopRuntime} from './runtimeEnvironment';
21
22
 
22
23
  export class NoopAnalytics implements AnalyticsClient {
@@ -68,5 +69,8 @@ export class NoopAnalytics implements AnalyticsClient {
68
69
  registerAfterSendEventHook(): void {}
69
70
  addEventTypeMapping(): void {}
70
71
  runtime = new NoopRuntime();
72
+ public get version(): string {
73
+ return libVersion;
74
+ }
71
75
  currentVisitorId = '';
72
76
  }
@@ -1,50 +1,27 @@
1
1
  interface CookieDetails {
2
2
  name: string;
3
3
  value: string;
4
- expires: string;
5
- domain: string;
4
+ expirationDate?: Date;
5
+ domain?: string;
6
6
  }
7
7
 
8
- // Code originally taken from : https://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/
8
+ // Code originally modified from : https://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/
9
9
  export class Cookie {
10
- static set(name: string, value: string, expiration?: number) {
11
- var domain: string, domainParts: string[], date: any, expires: string, host: string;
12
-
13
- if (expiration) {
14
- date = new Date();
15
- date.setTime(date.getTime() + expiration);
16
- expires = '; expires=' + date.toGMTString();
17
- } else {
18
- expires = '';
10
+ static set(name: string, value: string, expire?: number) {
11
+ var domain: string, expirationDate: Date | undefined, domainParts: string[], host: string;
12
+ if (expire) {
13
+ expirationDate = new Date();
14
+ expirationDate.setTime(expirationDate.getTime() + expire);
19
15
  }
20
-
21
16
  host = window.location.hostname;
22
17
  if (host.indexOf('.') === -1) {
23
- // no "." in a domain - it's localhost or something similar
24
- document.cookie = name + '=' + value + expires + '; path=/';
18
+ // no "." in a domain - single domain name, it's localhost or something similar
19
+ writeCookie(name, value, expirationDate);
25
20
  } else {
26
- // Remember the cookie on all subdomains.
27
- //
28
- // Start with trying to set cookie to the top domain.
29
- // (example: if user is on foo.com, try to set
30
- // cookie to domain ".com")
31
- //
32
- // If the cookie will not be set, it means ".com"
33
- // is a top level domain and we need to
34
- // set the cookie to ".foo.com"
35
21
  domainParts = host.split('.');
36
- domainParts.shift();
37
- domain = '.' + domainParts.join('.');
38
-
39
- writeCookie({name, value, expires, domain});
40
-
41
- // check if cookie was successfuly set to the given domain
42
- // (otherwise it was a Top-Level Domain)
43
- if (Cookie.get(name) == null || Cookie.get(name) != value) {
44
- // append "." to current domain
45
- domain = '.' + host;
46
- writeCookie({name, value, expires, domain});
47
- }
22
+ // we always have at least 2 domain parts
23
+ domain = domainParts[domainParts.length - 2] + '.' + domainParts[domainParts.length - 1];
24
+ writeCookie(name, value, expirationDate, domain);
48
25
  }
49
26
  }
50
27
 
@@ -53,9 +30,8 @@ export class Cookie {
53
30
  var cookieArray = document.cookie.split(';');
54
31
  for (var i = 0; i < cookieArray.length; i++) {
55
32
  var cookie = cookieArray[i];
56
- cookie = cookie.replace(/^\s+/, '');
57
-
58
- if (cookie.indexOf(cookiePrefix) == 0) {
33
+ cookie = cookie.replace(/^\s+/, ''); //strip whitespace from front of cookie only
34
+ if (cookie.lastIndexOf(cookiePrefix, 0) === 0) {
59
35
  return cookie.substring(cookiePrefix.length, cookie.length);
60
36
  }
61
37
  }
@@ -67,7 +43,10 @@ export class Cookie {
67
43
  }
68
44
  }
69
45
 
70
- function writeCookie(details: CookieDetails) {
71
- const {name, value, expires, domain} = details;
72
- document.cookie = `${name}=${value}${expires}; path=/; domain=${domain}; SameSite=Lax`;
46
+ function writeCookie(name: string, value: string, expirationDate?: Date, domain?: string) {
47
+ document.cookie =
48
+ `${name}=${value}` +
49
+ (expirationDate ? `;expires=${expirationDate.toUTCString()}` : '') +
50
+ (domain ? `;domain=${domain}` : '') +
51
+ ';SameSite=Lax';
73
52
  }
@@ -4,13 +4,13 @@ import handleOneAnalyticsEvent from './simpleanalytics';
4
4
  declare const self: any;
5
5
 
6
6
  const promise = (window as any)['Promise'];
7
- if (!(promise instanceof Function)) {
7
+ if (!(promise instanceof Function) && !global) {
8
8
  console.error(
9
9
  `This script uses window.Promise which is not supported in your browser. Consider adding a polyfill like "es6-promise".`
10
10
  );
11
11
  }
12
12
  const fetch = (window as any)['fetch'];
13
- if (!(fetch instanceof Function)) {
13
+ if (!(fetch instanceof Function) && !global) {
14
14
  console.error(
15
15
  `This script uses window.fetch which is not supported in your browser. Consider adding a polyfill like "fetch".`
16
16
  );
@@ -20,7 +20,7 @@ export class Plugins {
20
20
  `No plugin named "${name}" is currently registered. If you use a custom plugin, use 'provide' first.`
21
21
  );
22
22
  }
23
- this.requiredPlugins[name] = new pluginClass(options);
23
+ this.requiredPlugins[name] = new (pluginClass as any)(options);
24
24
  }
25
25
 
26
26
  provide(name: string, plugin: PluginClass) {
@@ -5,6 +5,7 @@ import {uuidv4} from '../client/crypto';
5
5
  import {PluginOptions} from '../plugins/BasePlugin';
6
6
  import {mockFetch} from '../../tests/fetchMock';
7
7
  import {CookieStorage} from '../storage';
8
+ import {libVersion} from '../version';
8
9
 
9
10
  const uuidv4Mock = jest.fn();
10
11
  jest.mock('../client/crypto', () => ({
@@ -376,6 +377,13 @@ describe('simpleanalytics', () => {
376
377
  });
377
378
  });
378
379
 
380
+ describe('version', () => {
381
+ it('returns the current version string', () => {
382
+ coveoua('init', 'MYTOKEN');
383
+ expect(coveoua('version')).toBe(libVersion);
384
+ });
385
+ });
386
+
379
387
  describe('callPlugin', () => {
380
388
  it('resolves properly plugin actions', () => {
381
389
  coveoua('provide', 'test', TestPluginWithSpy);
@@ -468,7 +476,7 @@ describe('simpleanalytics', () => {
468
476
  coveoua('init', 'SOME TOKEN', {plugins: ['svc']});
469
477
 
470
478
  expect(() => coveoua('potato')).toThrow(
471
- `The action "potato" does not exist. Available actions: init, set, send, onLoad, callPlugin, reset, require, provide.`
479
+ `The action "potato" does not exist. Available actions: init, set, send, onLoad, callPlugin, reset, require, provide, version.`
472
480
  );
473
481
  });
474
482
 
@@ -3,6 +3,7 @@ import {AnalyticsClient, CoveoAnalyticsClient, Endpoints} from '../client/analyt
3
3
  import {Plugins} from './plugins';
4
4
  import {PluginOptions} from '../plugins/BasePlugin';
5
5
  import {PluginClass} from '../plugins/BasePlugin';
6
+ import {libVersion} from '../version';
6
7
 
7
8
  export type AvailableActions = keyof CoveoUA;
8
9
 
@@ -137,6 +138,10 @@ export class CoveoUA {
137
138
  this.plugins = new Plugins();
138
139
  this.params = {};
139
140
  }
141
+
142
+ version(): string {
143
+ return libVersion;
144
+ }
140
145
  }
141
146
 
142
147
  export const coveoua = new CoveoUA();
@@ -160,6 +165,7 @@ export const handleOneAnalyticsEvent = (command: string, ...params: any[]) => {
160
165
  'reset',
161
166
  'require',
162
167
  'provide',
168
+ 'version',
163
169
  ];
164
170
  throw new Error(`The action "${command}" does not exist. Available actions: ${actions.join(', ')}.`);
165
171
  }
@@ -1,4 +1,5 @@
1
1
  import {CookieStorage, CookieAndLocalStorage} from './storage';
2
+ import {Cookie} from './cookieutils';
2
3
 
3
4
  describe('CookieStorage', () => {
4
5
  const key = 'wow';
@@ -16,6 +17,13 @@ describe('CookieStorage', () => {
16
17
  storage.removeItem(key);
17
18
  expect(storage.getItem(key)).toBe(null);
18
19
  });
20
+
21
+ it('honors expiration date', async () => {
22
+ const storage = new CookieStorage();
23
+ storage.setItem(key, someData, 1000);
24
+ await new Promise((res) => setTimeout(res, 1000)); // wait for 1 sec
25
+ expect(storage.getItem(key)).toBe(null);
26
+ });
19
27
  });
20
28
 
21
29
  describe('CookieAndLocalStorage', () => {
package/src/storage.ts CHANGED
@@ -33,8 +33,8 @@ export class CookieStorage implements WebStorage {
33
33
  removeItem(key: string) {
34
34
  Cookie.erase(`${CookieStorage.prefix}${key}`);
35
35
  }
36
- setItem(key: string, data: string): void {
37
- Cookie.set(`${CookieStorage.prefix}${key}`, data);
36
+ setItem(key: string, data: string, expire?: number): void {
37
+ Cookie.set(`${CookieStorage.prefix}${key}`, data, expire);
38
38
  }
39
39
  }
40
40
 
@@ -52,7 +52,7 @@ export class CookieAndLocalStorage implements WebStorage {
52
52
 
53
53
  setItem(key: string, data: string): void {
54
54
  localStorage.setItem(key, data);
55
- this.cookieStorage.setItem(key, data);
55
+ this.cookieStorage.setItem(key, data, 31556926000); // 1 year first party cookie
56
56
  }
57
57
  }
58
58
 
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const libVersion = process.env.PKG_VERSION || 'local'; // processed by @rollup/plugin-replace