coveo.analytics 2.30.56 → 2.32.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.
Files changed (84) hide show
  1. package/README.md +37 -43
  2. package/dist/browser.mjs +513 -357
  3. package/dist/coveoua.browser.js +1 -1
  4. package/dist/coveoua.browser.js.map +1 -1
  5. package/dist/coveoua.debug.js +542 -359
  6. package/dist/coveoua.debug.js.map +1 -1
  7. package/dist/coveoua.js +1 -1
  8. package/dist/coveoua.js.map +1 -1
  9. package/dist/definitions/client/analytics.d.ts +3 -2
  10. package/dist/definitions/client/runtimeEnvironment.d.ts +1 -1
  11. package/dist/definitions/donottrack.d.ts +1 -0
  12. package/dist/definitions/react-native/index.d.ts +1 -2
  13. package/dist/definitions/version.d.ts +1 -1
  14. package/dist/library.cjs +15350 -3699
  15. package/dist/library.es.js +513 -357
  16. package/dist/library.js +15350 -3699
  17. package/dist/library.mjs +15350 -3699
  18. package/dist/react-native.es.js +634 -460
  19. package/modules/package.json +9 -9
  20. package/package.json +60 -91
  21. package/react-native/package.json +7 -7
  22. package/src/caseAssist/caseAssistActions.ts +51 -50
  23. package/src/caseAssist/caseAssistClient.spec.ts +328 -297
  24. package/src/caseAssist/caseAssistClient.ts +201 -185
  25. package/src/client/analytics.spec.ts +724 -703
  26. package/src/client/analytics.ts +677 -602
  27. package/src/client/analyticsBeaconClient.spec.ts +195 -188
  28. package/src/client/analyticsBeaconClient.ts +99 -88
  29. package/src/client/analyticsFetchClient.ts +77 -61
  30. package/src/client/analyticsRequestClient.ts +17 -17
  31. package/src/client/location.ts +3 -3
  32. package/src/client/measurementProtocolMapper.ts +54 -45
  33. package/src/client/measurementProtocolMapping/baseMeasurementProtocolMapper.ts +48 -44
  34. package/src/client/measurementProtocolMapping/commerceMeasurementProtocolMapper.ts +133 -121
  35. package/src/client/measurementProtocolMapping/serviceMeasurementProtocolMapper.ts +17 -17
  36. package/src/client/noopAnalytics.ts +80 -68
  37. package/src/client/runtimeEnvironment.ts +50 -41
  38. package/src/client/utils.spec.ts +112 -97
  39. package/src/client/utils.ts +39 -39
  40. package/src/cookieutils.spec.ts +59 -0
  41. package/src/cookieutils.ts +40 -39
  42. package/src/coveoua/browser.ts +14 -12
  43. package/src/coveoua/library.ts +4 -4
  44. package/src/coveoua/plugins.ts +36 -34
  45. package/src/coveoua/simpleanalytics.spec.ts +467 -452
  46. package/src/coveoua/simpleanalytics.ts +146 -140
  47. package/src/detector.ts +17 -17
  48. package/src/donottrack.spec.ts +199 -121
  49. package/src/donottrack.ts +31 -8
  50. package/src/events.ts +86 -79
  51. package/src/formatting/format-array-for-coveo-custom-data.spec.ts +13 -12
  52. package/src/formatting/format-array-for-coveo-custom-data.ts +18 -18
  53. package/src/formatting/format-omnibox-metadata.ts +16 -12
  54. package/src/history.spec.ts +197 -195
  55. package/src/history.ts +143 -133
  56. package/src/hook/addDefaultValues.ts +13 -8
  57. package/src/hook/enhanceViewEvent.ts +17 -17
  58. package/src/insight/insightClient.spec.ts +1848 -1717
  59. package/src/insight/insightClient.ts +866 -761
  60. package/src/insight/insightEvents.ts +57 -56
  61. package/src/plugins/BasePlugin.ts +125 -121
  62. package/src/plugins/ec.spec.ts +457 -429
  63. package/src/plugins/ec.ts +202 -199
  64. package/src/plugins/link.spec.ts +183 -159
  65. package/src/plugins/link.ts +88 -85
  66. package/src/plugins/svc.spec.ts +161 -155
  67. package/src/plugins/svc.ts +93 -91
  68. package/src/react-native/index.ts +8 -1
  69. package/src/react-native/react-native-runtime.ts +33 -33
  70. package/src/react-native/react-native-utils.ts +1 -1
  71. package/src/react-native/react-native.spec.ts +31 -21
  72. package/src/searchPage/searchPageClient.spec.ts +1944 -1794
  73. package/src/searchPage/searchPageClient.ts +1261 -1040
  74. package/src/searchPage/searchPageEvents.ts +524 -511
  75. package/src/storage.spec.ts +51 -51
  76. package/src/storage.ts +47 -47
  77. package/dist/definitions/bundle/browser-fetch.d.ts +0 -1
  78. package/dist/definitions/src/coveoua/browser.d.ts +0 -6
  79. package/dist/definitions/src/coveoua/headless.d.ts +0 -6
  80. package/dist/definitions/src/coveoua/library.d.ts +0 -17
  81. package/dist/definitions/src/coveoua/plugins.d.ts +0 -10
  82. package/dist/definitions/src/coveoua/simpleanalytics.d.ts +0 -33
  83. package/dist/definitions/src/react-native/index.d.ts +0 -3
  84. package/dist/definitions/src/react-native/react-native-runtime.d.ts +0 -19
@@ -1,10 +1,11 @@
1
+ import {vi} from 'vitest';
1
2
  import {
2
- ClickEventRequest,
3
- CustomEventRequest,
4
- DefaultEventResponse,
5
- EventType,
6
- SearchEventRequest,
7
- ViewEventRequest,
3
+ ClickEventRequest,
4
+ CustomEventRequest,
5
+ DefaultEventResponse,
6
+ EventType,
7
+ SearchEventRequest,
8
+ ViewEventRequest,
8
9
  } from '../events';
9
10
  import {CoveoAnalyticsClient} from './analytics';
10
11
  import {IAnalyticsRequestOptions} from './analyticsRequestClient';
@@ -17,824 +18,844 @@ import {Cookie} from '../cookieutils';
17
18
  import {CoveoLinkParam} from '../plugins/link';
18
19
 
19
20
  const aVisitorId = '123';
20
- jest.mock('uuid', () => ({
21
+ vi.mock('uuid', async (importOriginal) => {
22
+ const actual = await importOriginal<typeof import('uuid')>();
23
+ return {
21
24
  v4: () => aVisitorId,
22
- validate: jest.requireActual('uuid').validate,
23
- v5: jest.requireActual('uuid').v5,
24
- }));
25
+ validate: actual.validate,
26
+ v5: actual.v5,
27
+ };
28
+ });
25
29
 
26
30
  const {fetchMock, fetchMockBeforeEach} = mockFetch();
27
31
 
28
32
  describe('Analytics', () => {
29
- const aToken = 'token';
30
- const anEndpoint = 'http://bloup';
31
- const A_VERSION = 'v1337';
32
-
33
- const viewEvent: ViewEventRequest = {
34
- location: 'here',
35
- contentIdKey: 'key',
36
- contentIdValue: 'value',
37
- language: 'en',
38
- };
39
- const searchEvent: SearchEventRequest = {
40
- searchQueryUid: 'sqUid',
41
- actionCause: 'interfaceLoad',
42
- queryText: 'test',
43
- responseTime: 50,
44
- };
45
- const clickEvent: ClickEventRequest = {
46
- searchQueryUid: 'sqUid',
47
- actionCause: 'documentOpen',
48
- documentPosition: 1,
49
- documentUriHash: 'docUriHash',
50
- sourceName: 'source',
51
- };
52
- const customEvent: CustomEventRequest = {
53
- eventType: 'custom',
54
- eventValue: 'value',
55
- };
56
-
57
- const eventResponse: DefaultEventResponse = {
58
- visitId: 'firsttimevisiting',
59
- visitorId: aVisitorId,
60
- };
61
-
62
- const endpointForEventType = (eventType: EventType, endpoint: string = `${anEndpoint}/rest`) =>
63
- `${endpoint}/${A_VERSION}/analytics/${eventType}?visitor=${aVisitorId}`;
64
- const mockFetchRequestForEventType = (eventType: EventType) => {
65
- const address = endpointForEventType(eventType);
66
- fetchMock.post(address, eventResponse);
67
- };
68
-
69
- let client: CoveoAnalyticsClient;
70
-
71
- beforeEach(() => {
72
- jest.resetAllMocks();
73
- fetchMockBeforeEach();
74
-
75
- new CookieStorage().removeItem('visitorId');
76
- localStorage.clear();
77
- fetchMock.reset();
78
-
79
- client = new CoveoAnalyticsClient({
80
- token: aToken,
81
- endpoint: anEndpoint,
82
- version: A_VERSION,
83
- });
33
+ const aToken = 'token';
34
+ const anEndpoint = 'http://bloup';
35
+ const A_VERSION = 'v1337';
36
+
37
+ const viewEvent: ViewEventRequest = {
38
+ location: 'here',
39
+ contentIdKey: 'key',
40
+ contentIdValue: 'value',
41
+ language: 'en',
42
+ };
43
+ const searchEvent: SearchEventRequest = {
44
+ searchQueryUid: 'sqUid',
45
+ actionCause: 'interfaceLoad',
46
+ queryText: 'test',
47
+ responseTime: 50,
48
+ };
49
+ const clickEvent: ClickEventRequest = {
50
+ searchQueryUid: 'sqUid',
51
+ actionCause: 'documentOpen',
52
+ documentPosition: 1,
53
+ documentUriHash: 'docUriHash',
54
+ sourceName: 'source',
55
+ };
56
+ const customEvent: CustomEventRequest = {
57
+ eventType: 'custom',
58
+ eventValue: 'value',
59
+ };
60
+
61
+ const eventResponse: DefaultEventResponse = {
62
+ visitId: 'firsttimevisiting',
63
+ visitorId: aVisitorId,
64
+ };
65
+
66
+ const endpointForEventType = (eventType: EventType, endpoint: string = `${anEndpoint}/rest`) =>
67
+ `${endpoint}/${A_VERSION}/analytics/${eventType}?visitor=${aVisitorId}`;
68
+ const mockFetchRequestForEventType = (eventType: EventType) => {
69
+ const address = endpointForEventType(eventType);
70
+ fetchMock.post(address, eventResponse);
71
+ };
72
+
73
+ let client: CoveoAnalyticsClient;
74
+
75
+ beforeEach(() => {
76
+ vi.resetAllMocks();
77
+ fetchMockBeforeEach();
78
+
79
+ new CookieStorage().removeItem('visitorId');
80
+ localStorage.clear();
81
+ fetchMock.reset();
82
+
83
+ client = new CoveoAnalyticsClient({
84
+ token: aToken,
85
+ endpoint: anEndpoint,
86
+ version: A_VERSION,
84
87
  });
88
+ });
85
89
 
86
- it('should add /rest if not present in endpoint', async () => {
87
- const legacyEndpoint = 'https://usageanalytics.com';
90
+ it('should add /rest if not present in endpoint', async () => {
91
+ const legacyEndpoint = 'https://usageanalytics.com';
88
92
 
89
- client = new CoveoAnalyticsClient({
90
- token: aToken,
91
- endpoint: legacyEndpoint,
92
- version: A_VERSION,
93
- });
94
-
95
- fetchMock.post(endpointForEventType(EventType.custom, `${legacyEndpoint}/rest`), eventResponse);
96
- const response = await client.sendEvent(EventType.custom);
97
- expect(response).toEqual(eventResponse);
93
+ client = new CoveoAnalyticsClient({
94
+ token: aToken,
95
+ endpoint: legacyEndpoint,
96
+ version: A_VERSION,
98
97
  });
99
98
 
100
- it('should not add /rest if /rest present in endpoint', async () => {
101
- const legacyEndpoint = 'https://usageanalytics.com/rest';
99
+ fetchMock.post(endpointForEventType(EventType.custom, `${legacyEndpoint}/rest`), eventResponse);
100
+ const response = await client.sendEvent(EventType.custom);
101
+ expect(response).toEqual(eventResponse);
102
+ });
102
103
 
103
- client = new CoveoAnalyticsClient({
104
- token: aToken,
105
- endpoint: legacyEndpoint,
106
- version: A_VERSION,
107
- });
104
+ it('should not add /rest if /rest present in endpoint', async () => {
105
+ const legacyEndpoint = 'https://usageanalytics.com/rest';
108
106
 
109
- fetchMock.post(endpointForEventType(EventType.custom, legacyEndpoint), eventResponse);
110
- const response = await client.sendEvent(EventType.custom);
111
- expect(response).toEqual(eventResponse);
107
+ client = new CoveoAnalyticsClient({
108
+ token: aToken,
109
+ endpoint: legacyEndpoint,
110
+ version: A_VERSION,
112
111
  });
113
112
 
114
- it('should not add /rest if /rest/ua present in endpoint', async () => {
115
- const legacyEndpoint = 'https://usageanalytics.com/rest/ua';
113
+ fetchMock.post(endpointForEventType(EventType.custom, legacyEndpoint), eventResponse);
114
+ const response = await client.sendEvent(EventType.custom);
115
+ expect(response).toEqual(eventResponse);
116
+ });
116
117
 
117
- client = new CoveoAnalyticsClient({
118
- token: aToken,
119
- endpoint: legacyEndpoint,
120
- version: A_VERSION,
121
- });
118
+ it('should not add /rest if /rest/ua present in endpoint', async () => {
119
+ const legacyEndpoint = 'https://usageanalytics.com/rest/ua';
122
120
 
123
- fetchMock.post(endpointForEventType(EventType.custom, legacyEndpoint), eventResponse);
124
- const response = await client.sendEvent(EventType.custom);
125
- expect(response).toEqual(eventResponse);
121
+ client = new CoveoAnalyticsClient({
122
+ token: aToken,
123
+ endpoint: legacyEndpoint,
124
+ version: A_VERSION,
126
125
  });
127
126
 
128
- it('should remove trailing slash in endpoint', async () => {
129
- const legacyEndpoint = 'https://usageanalytics.com';
127
+ fetchMock.post(endpointForEventType(EventType.custom, legacyEndpoint), eventResponse);
128
+ const response = await client.sendEvent(EventType.custom);
129
+ expect(response).toEqual(eventResponse);
130
+ });
130
131
 
131
- client = new CoveoAnalyticsClient({
132
- token: aToken,
133
- endpoint: legacyEndpoint + '/',
134
- version: A_VERSION,
135
- });
132
+ it('should remove trailing slash in endpoint', async () => {
133
+ const legacyEndpoint = 'https://usageanalytics.com';
136
134
 
137
- fetchMock.post(endpointForEventType(EventType.custom, `${legacyEndpoint}/rest`), eventResponse);
138
- const response = await client.sendEvent(EventType.custom);
139
- expect(response).toEqual(eventResponse);
135
+ client = new CoveoAnalyticsClient({
136
+ token: aToken,
137
+ endpoint: legacyEndpoint + '/',
138
+ version: A_VERSION,
140
139
  });
141
140
 
142
- it('should allow reverse-proxy endpoint', async () => {
143
- const aReverseProxyEndpoint = 'https://12364734.cloudfront.net/analytics';
141
+ fetchMock.post(endpointForEventType(EventType.custom, `${legacyEndpoint}/rest`), eventResponse);
142
+ const response = await client.sendEvent(EventType.custom);
143
+ expect(response).toEqual(eventResponse);
144
+ });
144
145
 
145
- client = new CoveoAnalyticsClient({
146
- token: aToken,
147
- endpoint: aReverseProxyEndpoint,
148
- isCustomEndpoint: true,
149
- version: A_VERSION,
150
- });
146
+ it('should allow reverse-proxy endpoint', async () => {
147
+ const aReverseProxyEndpoint = 'https://12364734.cloudfront.net/analytics';
151
148
 
152
- fetchMock.post(endpointForEventType(EventType.custom, aReverseProxyEndpoint), eventResponse);
153
- const response = await client.sendEvent(EventType.custom);
154
- expect(response).toEqual(eventResponse);
149
+ client = new CoveoAnalyticsClient({
150
+ token: aToken,
151
+ endpoint: aReverseProxyEndpoint,
152
+ isCustomEndpoint: true,
153
+ version: A_VERSION,
155
154
  });
156
155
 
157
- it('should call fetch with the parameters', async () => {
158
- mockFetchRequestForEventType(EventType.view);
156
+ fetchMock.post(endpointForEventType(EventType.custom, aReverseProxyEndpoint), eventResponse);
157
+ const response = await client.sendEvent(EventType.custom);
158
+ expect(response).toEqual(eventResponse);
159
+ });
159
160
 
160
- const response = await client.sendViewEvent(viewEvent);
161
- expect(response).toEqual(eventResponse);
161
+ it('should call fetch with the parameters', async () => {
162
+ mockFetchRequestForEventType(EventType.view);
162
163
 
163
- expect(fetchMock.called()).toBe(true);
164
+ const response = await client.sendViewEvent(viewEvent);
165
+ expect(response).toEqual(eventResponse);
164
166
 
165
- const [path, {headers, body}]: any = fetchMock.lastCall();
166
- expect(path).toBe(endpointForEventType(EventType.view));
167
+ expect(fetchMock.called()).toBe(true);
167
168
 
168
- const h = headers as Record<string, string>;
169
- expect(h['Authorization']).toBe(`Bearer ${aToken}`);
170
- expect(h['Content-Type']).toBe('application/json');
169
+ const [path, {headers, body}]: any = fetchMock.lastCall();
170
+ expect(path).toBe(endpointForEventType(EventType.view));
171
171
 
172
- expect(body).not.toBeUndefined();
172
+ const h = headers as Record<string, string>;
173
+ expect(h['Authorization']).toBe(`Bearer ${aToken}`);
174
+ expect(h['Content-Type']).toBe('application/json');
173
175
 
174
- const parsedBody = JSON.parse(body.toString());
175
- expect(parsedBody).toMatchObject(viewEvent);
176
- });
176
+ expect(body).not.toBeUndefined();
177
177
 
178
- it('should append default values to the view event', async () => {
179
- mockFetchRequestForEventType(EventType.view);
178
+ const parsedBody = JSON.parse(body.toString());
179
+ expect(parsedBody).toMatchObject(viewEvent);
180
+ });
180
181
 
181
- await client.sendViewEvent(viewEvent);
182
+ it('should append default values to the view event', async () => {
183
+ mockFetchRequestForEventType(EventType.view);
182
184
 
183
- const [body] = getParsedBodyCalls();
185
+ await client.sendViewEvent(viewEvent);
184
186
 
185
- expect(body).toMatchObject({
186
- clientId: '123',
187
- userAgent: navigator.userAgent,
188
- });
187
+ const [body] = getParsedBodyCalls();
188
+
189
+ expect(body).toMatchObject({
190
+ clientId: '123',
191
+ userAgent: navigator.userAgent,
189
192
  });
193
+ });
190
194
 
191
- it('should append default values to the search event', async () => {
192
- mockFetchRequestForEventType(EventType.search);
195
+ it('should append default values to the search event', async () => {
196
+ mockFetchRequestForEventType(EventType.search);
193
197
 
194
- await client.sendSearchEvent(searchEvent);
198
+ await client.sendSearchEvent(searchEvent);
195
199
 
196
- const [body] = getParsedBodyCalls();
200
+ const [body] = getParsedBodyCalls();
197
201
 
198
- expect(body).toMatchObject({
199
- clientId: '123',
200
- userAgent: navigator.userAgent,
201
- });
202
+ expect(body).toMatchObject({
203
+ clientId: '123',
204
+ userAgent: navigator.userAgent,
202
205
  });
206
+ });
203
207
 
204
- it('should append default values to the click event', async () => {
205
- mockFetchRequestForEventType(EventType.click);
208
+ it('should append default values to the click event', async () => {
209
+ mockFetchRequestForEventType(EventType.click);
206
210
 
207
- await client.sendClickEvent(clickEvent);
211
+ await client.sendClickEvent(clickEvent);
208
212
 
209
- const [body] = getParsedBodyCalls();
213
+ const [body] = getParsedBodyCalls();
210
214
 
211
- expect(body).toMatchObject({
212
- clientId: '123',
213
- userAgent: navigator.userAgent,
214
- });
215
+ expect(body).toMatchObject({
216
+ clientId: '123',
217
+ userAgent: navigator.userAgent,
215
218
  });
219
+ });
216
220
 
217
- it('should append default values to the custom event', async () => {
218
- mockFetchRequestForEventType(EventType.custom);
221
+ it('should append default values to the custom event', async () => {
222
+ mockFetchRequestForEventType(EventType.custom);
219
223
 
220
- await client.sendCustomEvent(customEvent);
224
+ await client.sendCustomEvent(customEvent);
221
225
 
222
- const [body] = getParsedBodyCalls();
226
+ const [body] = getParsedBodyCalls();
223
227
 
224
- expect(body).toMatchObject({
225
- clientId: '123',
226
- userAgent: navigator.userAgent,
227
- });
228
+ expect(body).toMatchObject({
229
+ clientId: '123',
230
+ userAgent: navigator.userAgent,
228
231
  });
232
+ });
229
233
 
230
- it('should send the events in the order they were called', async () => {
231
- mockFetchRequestForEventType(EventType.view);
232
- const firstRequest = client.sendViewEvent({
233
- ...viewEvent,
234
- customData: {
235
- index: 0,
236
- },
237
- });
238
- const secondRequest = client.sendViewEvent({
239
- ...viewEvent,
240
- customData: {
241
- index: 1,
242
- },
243
- });
244
- const thirdRequest = client.sendViewEvent({
245
- ...viewEvent,
246
- customData: {
247
- index: 2,
248
- },
249
- });
250
-
251
- await Promise.all([firstRequest, secondRequest, thirdRequest]);
234
+ it('should send the events in the order they were called', async () => {
235
+ mockFetchRequestForEventType(EventType.view);
236
+ const firstRequest = client.sendViewEvent({
237
+ ...viewEvent,
238
+ customData: {
239
+ index: 0,
240
+ },
241
+ });
242
+ const secondRequest = client.sendViewEvent({
243
+ ...viewEvent,
244
+ customData: {
245
+ index: 1,
246
+ },
247
+ });
248
+ const thirdRequest = client.sendViewEvent({
249
+ ...viewEvent,
250
+ customData: {
251
+ index: 2,
252
+ },
253
+ });
252
254
 
253
- const assertResponseHasCustomDataWithIndex = (response: RequestInit | undefined, index: number) => {
254
- const parsedBody = JSON.parse(response!.body!.toString());
255
- expect(parsedBody.customData.index).toBe(index);
256
- };
257
- const [[, firstResponse], [, secondResponse], [, thirdResponse]] = fetchMock.calls();
258
- assertResponseHasCustomDataWithIndex(firstResponse, 0);
259
- assertResponseHasCustomDataWithIndex(secondResponse, 1);
260
- assertResponseHasCustomDataWithIndex(thirdResponse, 2);
261
- });
262
-
263
- it('should should only clean null, undefined, and empty string values', async () => {
264
- mockFetchRequestForEventType(EventType.view);
265
- await client.sendEvent(EventType.view, {
266
- fine: 1,
267
- ok: 0,
268
- notok: '',
269
- invalid: null,
270
- ohno: undefined,
271
- });
255
+ await Promise.all([firstRequest, secondRequest, thirdRequest]);
272
256
 
273
- const [body] = getParsedBodyCalls();
257
+ const assertResponseHasCustomDataWithIndex = (
258
+ response: RequestInit | undefined,
259
+ index: number
260
+ ) => {
261
+ const parsedBody = JSON.parse(response!.body!.toString());
262
+ expect(parsedBody.customData.index).toBe(index);
263
+ };
264
+ const [[, firstResponse], [, secondResponse], [, thirdResponse]] = fetchMock.calls();
265
+ assertResponseHasCustomDataWithIndex(firstResponse, 0);
266
+ assertResponseHasCustomDataWithIndex(secondResponse, 1);
267
+ assertResponseHasCustomDataWithIndex(thirdResponse, 2);
268
+ });
269
+
270
+ it('should should only clean null, undefined, and empty string values', async () => {
271
+ mockFetchRequestForEventType(EventType.view);
272
+ await client.sendEvent(EventType.view, {
273
+ fine: 1,
274
+ ok: 0,
275
+ emptyField: '',
276
+ invalid: null,
277
+ undefinedField: undefined,
278
+ });
279
+
280
+ const [body] = getParsedBodyCalls();
281
+
282
+ expect(body).toMatchObject({
283
+ fine: 1,
284
+ ok: 0,
285
+ });
286
+ });
287
+
288
+ describe('should truncate the maxlength for URL parameters at 1024 characters for ua events', () => {
289
+ const desiredMax: number = 1024;
290
+ // Craft the URL so the truncation point falls in the %20 sequence
291
+ const longUrl: string = 'http://coveo.com/?q=' + 'a'.repeat(desiredMax - 22) + '%20b';
292
+ const expectedTruncatedLength = longUrl.lastIndexOf('%', desiredMax);
293
+ expect(longUrl.length).toBeGreaterThan(desiredMax);
294
+ async function testEventType(type: EventType, url: string) {
295
+ mockFetchRequestForEventType(type);
296
+ await client.sendEvent(type, {
297
+ location: type == EventType.view ? url : undefined,
298
+ originLevel3: url,
299
+ });
300
+ const [body] = getParsedBodyCalls();
301
+ if (type == EventType.view) {
302
+ expect(body.location).toHaveLength(expectedTruncatedLength);
303
+ }
304
+ expect(body.originLevel3).toHaveLength(expectedTruncatedLength);
305
+ }
306
+ it('for view events', () => testEventType(EventType.view, longUrl));
307
+ it('for click events', () => testEventType(EventType.click, longUrl));
308
+ it('for search events', () => testEventType(EventType.search, longUrl));
309
+ it('for custom events', () => testEventType(EventType.custom, longUrl));
310
+ });
311
+
312
+ describe('url truncation is null safe', () => {
313
+ async function testAttributeTruncation(url: any) {
314
+ mockFetchRequestForEventType(EventType.view);
315
+ await client.sendEvent(EventType.view, {
316
+ location: url,
317
+ originLevel3: url,
318
+ });
319
+ const [body] = getParsedBodyCalls();
320
+ expect(body.location).toBe(url == null ? undefined : url);
321
+ expect(body.originLevel3).toBe(url == null ? undefined : url);
322
+ }
323
+ it('for undefined urls', () => testAttributeTruncation(undefined));
324
+ it('for null urls', () => testAttributeTruncation(null));
325
+ it('for non-string urls', () => testAttributeTruncation(12345));
326
+ });
274
327
 
275
- expect(body).toMatchObject({
276
- fine: 1,
277
- ok: 0,
278
- });
328
+ it('should not remove #queryText for search events even if empty', async () => {
329
+ mockFetchRequestForEventType(EventType.search);
330
+ await client.sendEvent(EventType.search, {
331
+ queryText: '',
279
332
  });
280
333
 
281
- describe('should truncate the maxlength for URL parameters at 1024 characters for ua events', () => {
282
- const desiredMax: number = 1024;
283
- // Craft the URL so the truncation point falls in the %20 sequence
284
- const longUrl: string = 'http://coveo.com/?q=' + 'a'.repeat(desiredMax - 22) + '%20b';
285
- const expectedTruncatedLength = longUrl.lastIndexOf('%', desiredMax);
286
- expect(longUrl.length).toBeGreaterThan(desiredMax);
287
- async function testEventType(type: EventType, url: string) {
288
- mockFetchRequestForEventType(type);
289
- await client.sendEvent(type, {
290
- location: type == EventType.view ? url : undefined,
291
- originLevel3: url,
292
- });
293
- const [body] = getParsedBodyCalls();
294
- if (type == EventType.view) {
295
- expect(body.location).toHaveLength(expectedTruncatedLength);
296
- }
297
- expect(body.originLevel3).toHaveLength(expectedTruncatedLength);
298
- }
299
- it('for view events', () => testEventType(EventType.view, longUrl));
300
- it('for click events', () => testEventType(EventType.click, longUrl));
301
- it('for search events', () => testEventType(EventType.search, longUrl));
302
- it('for custom events', () => testEventType(EventType.custom, longUrl));
303
- });
304
-
305
- describe('url truncation is null safe', () => {
306
- async function testAttributeTruncation(url: any) {
307
- mockFetchRequestForEventType(EventType.view);
308
- await client.sendEvent(EventType.view, {
309
- location: url,
310
- originLevel3: url,
311
- });
312
- const [body] = getParsedBodyCalls();
313
- expect(body.location).toBe(url == null ? undefined : url);
314
- expect(body.originLevel3).toBe(url == null ? undefined : url);
315
- }
316
- it('for undefined urls', () => testAttributeTruncation(undefined));
317
- it('for null urls', () => testAttributeTruncation(null));
318
- it('for non-string urls', () => testAttributeTruncation(12345));
319
- });
320
-
321
- it('should not remove #queryText for search events even if empty', async () => {
322
- mockFetchRequestForEventType(EventType.search);
323
- await client.sendEvent(EventType.search, {
324
- queryText: '',
325
- });
334
+ const [body] = getParsedBodyCalls();
326
335
 
327
- const [body] = getParsedBodyCalls();
328
-
329
- expect(body).toMatchObject({
330
- queryText: '',
331
- });
336
+ expect(body).toMatchObject({
337
+ queryText: '',
332
338
  });
339
+ });
333
340
 
334
- describe('with event type mapping with variable arguments', () => {
335
- const specialEventType = '🌟special🌟';
336
- const argumentNames = ['eventCategory', 'eventAction', 'eventLabel', 'eventValue'];
337
- beforeEach(() => {
338
- mockFetchRequestForEventType(EventType.custom);
339
- client.addEventTypeMapping(specialEventType, {
340
- newEventType: EventType.custom,
341
- variableLengthArgumentsNames: argumentNames,
342
- });
343
- });
341
+ describe('with event type mapping with variable arguments', () => {
342
+ const specialEventType = '🌟special🌟';
343
+ const argumentNames = ['eventCategory', 'eventAction', 'eventLabel', 'eventValue'];
344
+ beforeEach(() => {
345
+ mockFetchRequestForEventType(EventType.custom);
346
+ client.addEventTypeMapping(specialEventType, {
347
+ newEventType: EventType.custom,
348
+ variableLengthArgumentsNames: argumentNames,
349
+ });
350
+ });
344
351
 
345
- it('should properly parse empty arguments', async () => {
346
- await client.sendEvent(specialEventType);
352
+ it('should properly parse empty arguments', async () => {
353
+ await client.sendEvent(specialEventType);
347
354
 
348
- const [path, {body}]: any = fetchMock.lastCall();
349
- expect(path).toBe(endpointForEventType(EventType.custom));
355
+ const [path, {body}]: any = fetchMock.lastCall();
356
+ expect(path).toBe(endpointForEventType(EventType.custom));
350
357
 
351
- const parsedBody = JSON.parse(body.toString());
352
- expect(parsedBody).toEqual({});
353
- });
358
+ const parsedBody = JSON.parse(body.toString());
359
+ expect(parsedBody).toEqual({});
360
+ });
354
361
 
355
- it('should properly parse 1 argument', async () => {
356
- await client.sendEvent(specialEventType, 'Video');
362
+ it('should properly parse 1 argument', async () => {
363
+ await client.sendEvent(specialEventType, 'Video');
357
364
 
358
- const [body] = getParsedBodyCalls();
365
+ const [body] = getParsedBodyCalls();
359
366
 
360
- expect(body).toEqual({
361
- [argumentNames[0]]: 'Video',
362
- });
363
- });
367
+ expect(body).toEqual({
368
+ [argumentNames[0]]: 'Video',
369
+ });
370
+ });
364
371
 
365
- it('should properly parse all arguments', async () => {
366
- await client.sendEvent(specialEventType, 'Video', 'play', 'campaign');
372
+ it('should properly parse all arguments', async () => {
373
+ await client.sendEvent(specialEventType, 'Video', 'play', 'campaign');
367
374
 
368
- const [body] = getParsedBodyCalls();
375
+ const [body] = getParsedBodyCalls();
369
376
 
370
- expect(body).toEqual({
371
- [argumentNames[0]]: 'Video',
372
- [argumentNames[1]]: 'play',
373
- [argumentNames[2]]: 'campaign',
374
- });
375
- });
377
+ expect(body).toEqual({
378
+ [argumentNames[0]]: 'Video',
379
+ [argumentNames[1]]: 'play',
380
+ [argumentNames[2]]: 'campaign',
381
+ });
382
+ });
376
383
 
377
- it('should properly handle a finished object argument', async () => {
378
- await client.sendEvent(specialEventType, 'Video', {
379
- nonInteraction: true,
380
- });
384
+ it('should properly handle a finished object argument', async () => {
385
+ await client.sendEvent(specialEventType, 'Video', {
386
+ nonInteraction: true,
387
+ });
381
388
 
382
- const [body] = getParsedBodyCalls();
389
+ const [body] = getParsedBodyCalls();
383
390
 
384
- expect(body).toEqual({
385
- [argumentNames[0]]: 'Video',
386
- nonInteraction: true,
387
- });
388
- });
391
+ expect(body).toEqual({
392
+ [argumentNames[0]]: 'Video',
393
+ nonInteraction: true,
394
+ });
389
395
  });
396
+ });
390
397
 
391
- describe('with event type mapping activating context values', () => {
392
- const specialEventType = '🌟special🌟';
393
- beforeEach(() => {
394
- mockFetchRequestForEventType(EventType.custom);
395
- client.addEventTypeMapping(specialEventType, {
396
- newEventType: EventType.custom,
397
- addVisitorIdParameter: true,
398
- });
399
- });
398
+ describe('with event type mapping activating context values', () => {
399
+ const specialEventType = '🌟special🌟';
400
+ beforeEach(() => {
401
+ mockFetchRequestForEventType(EventType.custom);
402
+ client.addEventTypeMapping(specialEventType, {
403
+ newEventType: EventType.custom,
404
+ addVisitorIdParameter: true,
405
+ });
406
+ });
400
407
 
401
- it('should properly add visitorId key', async () => {
402
- await client.sendEvent(specialEventType);
408
+ it('should properly add visitorId key', async () => {
409
+ await client.sendEvent(specialEventType);
403
410
 
404
- const [body] = getParsedBodyCalls();
411
+ const [body] = getParsedBodyCalls();
405
412
 
406
- expect(body).toHaveProperty('visitorId');
407
- });
413
+ expect(body).toHaveProperty('visitorId');
408
414
  });
415
+ });
409
416
 
410
- describe('with an endpoint set to the coveo platform proxy', () => {
411
- const endpointWithRest = 'http://someendpoint.cloud.coveo.com/rest/ua';
412
- beforeEach(() => {
413
- client = new CoveoAnalyticsClient({
414
- token: aToken,
415
- endpoint: endpointWithRest,
416
- version: A_VERSION,
417
- });
418
-
419
- fetchMock.post(endpointForEventType(EventType.custom, endpointWithRest), eventResponse);
420
- });
417
+ describe('with an endpoint set to the coveo platform proxy', () => {
418
+ const endpointWithRest = 'http://someendpoint.cloud.coveo.com/rest/ua';
419
+ beforeEach(() => {
420
+ client = new CoveoAnalyticsClient({
421
+ token: aToken,
422
+ endpoint: endpointWithRest,
423
+ version: A_VERSION,
424
+ });
421
425
 
422
- it('should properly send the event without appending an extra /rest', async () => {
423
- const response = await client.sendEvent(EventType.custom);
424
- expect(response).toEqual(eventResponse);
425
- });
426
+ fetchMock.post(endpointForEventType(EventType.custom, endpointWithRest), eventResponse);
426
427
  });
427
428
 
428
- describe('with userId auto-detection', () => {
429
- const eventType = '🛂';
429
+ it('should properly send the event without appending an extra /rest', async () => {
430
+ const response = await client.sendEvent(EventType.custom);
431
+ expect(response).toEqual(eventResponse);
432
+ });
433
+ });
430
434
 
431
- const expectUserId = (userId: {[key: string]: string}) => {
432
- const [body] = getParsedBodyCalls();
433
- expect(body).toMatchObject(userId);
434
- };
435
+ describe('with userId auto-detection', () => {
436
+ const eventType = '🛂';
435
437
 
436
- describe('for API keys', () => {
437
- beforeEach(() => {
438
- client = new CoveoAnalyticsClient({
439
- token: 'xxapikey',
440
- endpoint: anEndpoint,
441
- version: A_VERSION,
442
- });
443
- mockFetchRequestForEventType(EventType.custom);
444
- });
445
- describe('with measurement protocol', () => {
446
- beforeEach(() => {
447
- client.addEventTypeMapping(eventType, {
448
- newEventType: EventType.custom,
449
- usesMeasurementProtocol: true,
450
- });
451
- });
452
-
453
- it('should set the absent userId to anonymous', async () => {
454
- await client.sendEvent(eventType);
455
- expectUserId({uid: 'anonymous'});
456
- });
457
-
458
- it('should leave existing userIds', async () => {
459
- await client.sendEvent(eventType, {userId: 'bob'});
460
- expectUserId({uid: 'bob'});
461
- });
462
- });
463
-
464
- describe('without measurement protocol', () => {
465
- beforeEach(() => {
466
- client.addEventTypeMapping(eventType, {
467
- newEventType: EventType.custom,
468
- usesMeasurementProtocol: false,
469
- });
470
- });
471
-
472
- it('should do nothing with absent userId', async () => {
473
- await client.sendEvent(eventType);
474
- expectUserId({});
475
- });
476
-
477
- it('should leave existing userIds', async () => {
478
- await client.sendEvent(eventType, {userId: 'bob'});
479
- expectUserId({userId: 'bob'});
480
- });
481
- });
482
- });
438
+ const expectUserId = (userId: {[key: string]: string}) => {
439
+ const [body] = getParsedBodyCalls();
440
+ expect(body).toMatchObject(userId);
441
+ };
483
442
 
484
- describe('for OAuth Tokens', () => {
485
- beforeEach(() => {
486
- client = new CoveoAnalyticsClient({
487
- token: 'xtoken',
488
- endpoint: anEndpoint,
489
- version: A_VERSION,
490
- });
491
- mockFetchRequestForEventType(EventType.custom);
492
- client.addEventTypeMapping(eventType, {
493
- newEventType: EventType.custom,
494
- usesMeasurementProtocol: true,
495
- });
496
- });
497
-
498
- it('should do nothing with absent userId', async () => {
499
- await client.sendEvent(eventType);
500
- expectUserId({});
501
- });
502
-
503
- it('should leave existing userIds', async () => {
504
- await client.sendEvent(eventType, {userId: 'bob'});
505
- expectUserId({uid: 'bob'});
506
- });
443
+ describe('for API keys', () => {
444
+ beforeEach(() => {
445
+ client = new CoveoAnalyticsClient({
446
+ token: 'xxTestApiKey',
447
+ endpoint: anEndpoint,
448
+ version: A_VERSION,
507
449
  });
508
- });
509
-
510
- describe('with context_website is set in customData', () => {
511
- const contextWebsite = 'yourbestfriend.com';
512
- const trackingId = 'yourfavoritefood.ca ';
450
+ mockFetchRequestForEventType(EventType.custom);
451
+ });
452
+ describe('with measurement protocol', () => {
513
453
  beforeEach(() => {
514
- client = new CoveoAnalyticsClient({
515
- token: 'xtoken',
516
- endpoint: anEndpoint,
517
- version: A_VERSION,
518
- });
519
- mockFetchRequestForEventType(EventType.view);
454
+ client.addEventTypeMapping(eventType, {
455
+ newEventType: EventType.custom,
456
+ usesMeasurementProtocol: true,
457
+ });
520
458
  });
521
459
 
522
- it('should set trackingId when trackingId is not specified', async () => {
523
- await client.sendEvent(EventType.view, {customData: {context_website: contextWebsite}});
524
- const [body] = getParsedBodyCalls();
525
- expect(body.trackingId).toBe(contextWebsite);
460
+ it('should set the absent userId to anonymous', async () => {
461
+ await client.sendEvent(eventType);
462
+ expectUserId({uid: 'anonymous'});
526
463
  });
527
464
 
528
- it('should not overwrite trackingId when trackingId is specified', async () => {
529
- await client.sendEvent(EventType.view, {
530
- trackingId: trackingId,
531
- customData: {context_website: contextWebsite},
532
- });
533
- const [body] = getParsedBodyCalls();
534
- expect(body.trackingId).toBe(trackingId);
465
+ it('should leave existing userIds', async () => {
466
+ await client.sendEvent(eventType, {userId: 'bob'});
467
+ expectUserId({uid: 'bob'});
535
468
  });
536
- });
469
+ });
537
470
 
538
- describe('with siteName is set in customData', () => {
539
- const website = 'yourbestfriend.com';
540
- const trackingId = 'yourfavoritefood.ca ';
471
+ describe('without measurement protocol', () => {
541
472
  beforeEach(() => {
542
- client = new CoveoAnalyticsClient({
543
- token: 'xtoken',
544
- endpoint: anEndpoint,
545
- version: A_VERSION,
546
- });
547
- mockFetchRequestForEventType(EventType.view);
473
+ client.addEventTypeMapping(eventType, {
474
+ newEventType: EventType.custom,
475
+ usesMeasurementProtocol: false,
476
+ });
548
477
  });
549
478
 
550
- it('should set trackingId when trackingId is not specified', async () => {
551
- await client.sendEvent(EventType.view, {customData: {siteName: website}});
552
- const [body] = getParsedBodyCalls();
553
- expect(body.trackingId).toBe(website);
479
+ it('should do nothing with absent userId', async () => {
480
+ await client.sendEvent(eventType);
481
+ expectUserId({});
554
482
  });
555
483
 
556
- it('should not overwrite trackingId when trackingId is specified', async () => {
557
- await client.sendEvent(EventType.view, {trackingId: trackingId, customData: {siteName: website}});
558
- const [body] = getParsedBodyCalls();
559
- expect(body.trackingId).toBe(trackingId);
484
+ it('should leave existing userIds', async () => {
485
+ await client.sendEvent(eventType, {userId: 'bob'});
486
+ expectUserId({userId: 'bob'});
560
487
  });
488
+ });
561
489
  });
562
490
 
563
- it('should support clearing cookies for visitorId and historyStore', () => {
564
- const visitorId = 'foo';
565
- const history = {name: 'foo', time: '123', value: 'bar'};
566
- const storage = new CookieStorage();
567
- const historyStore = new HistoryStore();
568
-
569
- client.currentVisitorId = visitorId;
570
- historyStore.addElement(history);
571
-
572
- expect(storage.getItem('visitorId')).toBe('foo');
573
- expect(historyStore.getMostRecentElement()).toEqual(history);
574
-
575
- client.clear();
576
- expect(storage.getItem('visitorId')).toBeNull();
577
- expect(historyStore.getMostRecentElement()).toBeUndefined();
578
- });
579
-
580
- it('should support clearing cookies for visitorId and historyStore async', async () => {
581
- const visitorId = 'foo';
582
- const history = {name: 'foo', time: '123', value: 'bar'};
583
- const storage = new CookieStorage();
584
- const historyStore = new HistoryStore();
585
-
586
- await client.setCurrentVisitorId(visitorId);
587
- await historyStore.addElementAsync(history);
588
-
589
- expect(await storage.getItem('visitorId')).toBe('foo');
590
- expect(historyStore.getMostRecentElement()).toEqual(history);
591
-
592
- client.clear();
593
- expect(storage.getItem('visitorId')).toBeNull();
594
- expect(historyStore.getMostRecentElement()).toBeUndefined();
595
- });
596
-
597
- it('should execute before send hooks passed as option', async () => {
598
- mockFetchRequestForEventType(EventType.search);
599
- const spy = jest.fn((_, p) => p);
600
- const searchEventPayload = {queryText: 'potato'};
601
- await new CoveoAnalyticsClient({
602
- token: aToken,
603
- endpoint: anEndpoint,
604
- version: A_VERSION,
605
- beforeSendHooks: [spy],
606
- }).sendEvent(EventType.search, searchEventPayload);
607
- expect(spy).toHaveBeenLastCalledWith(EventType.search, expect.objectContaining(searchEventPayload));
608
- });
609
-
610
- it('should preprocess the request with the preprocessRequest', async () => {
611
- let clientOrigin: string;
612
- let processedRequest: IAnalyticsRequestOptions = {
613
- url: 'https://www.myownanalytics.com/endpoint',
614
- headers: {
615
- Age: '24',
616
- },
617
- method: 'PUT',
618
- body: JSON.stringify({
619
- test: 'custom',
620
- }),
621
- };
622
- fetchMock.put(processedRequest.url, eventResponse);
623
- const searchEventPayload = {queryText: 'potato'};
624
- await new CoveoAnalyticsClient({
625
- token: aToken,
626
- endpoint: anEndpoint,
627
- version: A_VERSION,
628
- preprocessRequest: (request, type) => {
629
- clientOrigin = type;
630
- processedRequest = {
631
- ...request,
632
- ...processedRequest,
633
- };
634
- return processedRequest;
635
- },
636
- }).sendEvent(EventType.search, searchEventPayload);
637
-
638
- const call = fetchMock.calls()[0];
639
- const url = call[0];
640
- const options: RequestInit | undefined = call[1];
641
-
642
- const {url: expectedUrl, ...expectedOptions} = processedRequest;
643
- expect(clientOrigin!).toBe('analyticsFetch');
644
- expect(url).toBe(expectedUrl);
645
- expect(options).toEqual(expectedOptions);
646
- });
647
-
648
- const getParsedBodyCalls = (): any[] => {
649
- return fetchMock.calls().map(([, {body}]: any) => {
650
- return JSON.parse(body.toString());
491
+ describe('for OAuth Tokens', () => {
492
+ beforeEach(() => {
493
+ client = new CoveoAnalyticsClient({
494
+ token: 'testToken',
495
+ endpoint: anEndpoint,
496
+ version: A_VERSION,
651
497
  });
652
- };
653
- });
654
-
655
- describe('doNotTrack', () => {
656
- it('should do business as usual if doNotTrack returns false', () => {
657
- jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => false);
658
-
659
- let client = new CoveoAnalyticsClient({});
660
-
661
- expect(client.runtime).toBeInstanceOf(BrowserRuntime);
662
- expect(client.runtime.storage).toBeInstanceOf(CookieAndLocalStorage);
663
- });
664
-
665
- it('should honor doNotTrack', () => {
666
- jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => true);
498
+ mockFetchRequestForEventType(EventType.custom);
499
+ client.addEventTypeMapping(eventType, {
500
+ newEventType: EventType.custom,
501
+ usesMeasurementProtocol: true,
502
+ });
503
+ });
667
504
 
668
- let client = new CoveoAnalyticsClient({});
505
+ it('should do nothing with absent userId', async () => {
506
+ await client.sendEvent(eventType);
507
+ expectUserId({});
508
+ });
669
509
 
670
- expect(client.runtime).toBeInstanceOf(NoopRuntime);
671
- expect(client.runtime.storage).toBeInstanceOf(NullStorage);
510
+ it('should leave existing userIds', async () => {
511
+ await client.sendEvent(eventType, {userId: 'bob'});
512
+ expectUserId({uid: 'bob'});
513
+ });
672
514
  });
515
+ });
673
516
 
674
- it('should not clear existing cookies', async () => {
675
- jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => true);
676
- Cookie.set('coveo_visitorId', aVisitorId);
677
- expect(Cookie.get('coveo_visitorId')).toBe(aVisitorId);
517
+ describe('with context_website is set in customData', () => {
518
+ const contextWebsite = 'yourbestfriend.com';
519
+ const trackingId = 'yourfavoritefood.ca ';
520
+ beforeEach(() => {
521
+ client = new CoveoAnalyticsClient({
522
+ token: 'testToken',
523
+ endpoint: anEndpoint,
524
+ version: A_VERSION,
525
+ });
526
+ mockFetchRequestForEventType(EventType.view);
527
+ });
528
+
529
+ it('should set trackingId when trackingId is not specified', async () => {
530
+ await client.sendEvent(EventType.view, {
531
+ customData: {context_website: contextWebsite},
532
+ });
533
+ const [body] = getParsedBodyCalls();
534
+ expect(body.trackingId).toBe(contextWebsite);
535
+ });
536
+
537
+ it('should not overwrite trackingId when trackingId is specified', async () => {
538
+ await client.sendEvent(EventType.view, {
539
+ trackingId: trackingId,
540
+ customData: {context_website: contextWebsite},
541
+ });
542
+ const [body] = getParsedBodyCalls();
543
+ expect(body.trackingId).toBe(trackingId);
544
+ });
545
+ });
546
+
547
+ describe('with siteName is set in customData', () => {
548
+ const website = 'yourbestfriend.com';
549
+ const trackingId = 'yourfavoritefood.ca ';
550
+ beforeEach(() => {
551
+ client = new CoveoAnalyticsClient({
552
+ token: 'testToken',
553
+ endpoint: anEndpoint,
554
+ version: A_VERSION,
555
+ });
556
+ mockFetchRequestForEventType(EventType.view);
557
+ });
558
+
559
+ it('should set trackingId when trackingId is not specified', async () => {
560
+ await client.sendEvent(EventType.view, {customData: {siteName: website}});
561
+ const [body] = getParsedBodyCalls();
562
+ expect(body.trackingId).toBe(website);
563
+ });
564
+
565
+ it('should not overwrite trackingId when trackingId is specified', async () => {
566
+ await client.sendEvent(EventType.view, {
567
+ trackingId: trackingId,
568
+ customData: {siteName: website},
569
+ });
570
+ const [body] = getParsedBodyCalls();
571
+ expect(body.trackingId).toBe(trackingId);
572
+ });
573
+ });
574
+
575
+ it('should support clearing cookies for visitorId and historyStore', () => {
576
+ const visitorId = 'foo';
577
+ const history = {name: 'foo', time: '123', value: 'bar'};
578
+ const storage = new CookieStorage();
579
+ const historyStore = new HistoryStore();
580
+
581
+ client.currentVisitorId = visitorId;
582
+ historyStore.addElement(history);
583
+
584
+ expect(storage.getItem('visitorId')).toBe('foo');
585
+ expect(historyStore.getMostRecentElement()).toEqual(history);
586
+
587
+ client.clear();
588
+ expect(storage.getItem('visitorId')).toBeNull();
589
+ expect(historyStore.getMostRecentElement()).toBeUndefined();
590
+ });
591
+
592
+ it('should support clearing cookies for visitorId and historyStore async', async () => {
593
+ const visitorId = 'foo';
594
+ const history = {name: 'foo', time: '123', value: 'bar'};
595
+ const storage = new CookieStorage();
596
+ const historyStore = new HistoryStore();
597
+
598
+ await client.setCurrentVisitorId(visitorId);
599
+ await historyStore.addElementAsync(history);
600
+
601
+ expect(await storage.getItem('visitorId')).toBe('foo');
602
+ expect(historyStore.getMostRecentElement()).toEqual(history);
603
+
604
+ client.clear();
605
+ expect(storage.getItem('visitorId')).toBeNull();
606
+ expect(historyStore.getMostRecentElement()).toBeUndefined();
607
+ });
608
+
609
+ it('should execute before send hooks passed as option', async () => {
610
+ mockFetchRequestForEventType(EventType.search);
611
+ const spy = vi.fn((_, p) => p);
612
+ const searchEventPayload = {queryText: 'potato'};
613
+ await new CoveoAnalyticsClient({
614
+ token: aToken,
615
+ endpoint: anEndpoint,
616
+ version: A_VERSION,
617
+ beforeSendHooks: [spy],
618
+ }).sendEvent(EventType.search, searchEventPayload);
619
+ expect(spy).toHaveBeenLastCalledWith(
620
+ EventType.search,
621
+ expect.objectContaining(searchEventPayload)
622
+ );
623
+ });
624
+
625
+ it('should preprocess the request with the preprocessRequest', async () => {
626
+ let clientOrigin: string;
627
+ let processedRequest: IAnalyticsRequestOptions = {
628
+ url: 'https://www.myownanalytics.com/endpoint',
629
+ headers: {
630
+ Age: '24',
631
+ },
632
+ method: 'PUT',
633
+ body: JSON.stringify({
634
+ test: 'custom',
635
+ }),
636
+ };
637
+ fetchMock.put(processedRequest.url, eventResponse);
638
+ const searchEventPayload = {queryText: 'potato'};
639
+ await new CoveoAnalyticsClient({
640
+ token: aToken,
641
+ endpoint: anEndpoint,
642
+ version: A_VERSION,
643
+ preprocessRequest: (request, type) => {
644
+ clientOrigin = type;
645
+ processedRequest = {
646
+ ...request,
647
+ ...processedRequest,
648
+ };
649
+ return processedRequest;
650
+ },
651
+ }).sendEvent(EventType.search, searchEventPayload);
678
652
 
679
- new CoveoAnalyticsClient({});
653
+ const call = fetchMock.calls()[0];
654
+ const url = call[0];
655
+ const options: RequestInit | undefined = call[1];
680
656
 
681
- expect(Cookie.get('coveo_visitorId')).toBe(aVisitorId);
682
- });
683
- });
657
+ const {url: expectedUrl, ...expectedOptions} = processedRequest;
658
+ expect(clientOrigin!).toBe('analyticsFetch');
659
+ expect(url).toBe(expectedUrl);
660
+ expect(options).toEqual(expectedOptions);
661
+ });
684
662
 
685
- describe('custom clientId', () => {
686
- it('allows setting of a custom clientId', async () => {
687
- const client = new CoveoAnalyticsClient({});
688
- client.setClientId('c7d57b22-4aa8-487a-a106-be5243885f0a');
689
- expect(await client.getCurrentVisitorId()).toBe('c7d57b22-4aa8-487a-a106-be5243885f0a');
690
- });
691
-
692
- it('persists clientId in lowercase', async () => {
693
- const client = new CoveoAnalyticsClient({});
694
- client.setClientId('C7D57B22-4AA8-487A-A106-BE5243885F0A');
695
- expect(await client.getCurrentVisitorId()).toBe('c7d57b22-4aa8-487a-a106-be5243885f0a');
696
- });
697
-
698
- it('allows setting a custom consistent clientId given a string', async () => {
699
- const client = new CoveoAnalyticsClient({});
700
- client.setClientId('somestring', 'testNameSpace');
701
- //uuid v5 specific uuid generation
702
- expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
703
- //check for consistent ids
704
- client.setClientId('somestring', 'testNameSpace');
705
- expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
706
- client.setClientId('otherstring', 'testNameSpace');
707
- expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
708
- client.setClientId('somestring', 'otherNameSpace');
709
- expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
710
- });
711
-
712
- it('errors when not providing a namespace', async () => {
713
- const client = new CoveoAnalyticsClient({});
714
- expect.assertions(1);
715
- await expect(client.setClientId('somestring')).rejects.toEqual(
716
- Error('Cannot generate uuid client id without a specific namespace string.'),
717
- );
718
- //uuid v5 specific uuid generation
663
+ const getParsedBodyCalls = (): any[] => {
664
+ return fetchMock.calls().map(([, {body}]: any) => {
665
+ return JSON.parse(body.toString());
719
666
  });
667
+ };
720
668
  });
721
669
 
722
- describe('clientId from link', () => {
723
- // note: referrer is set as http://somewhere.over/thereferrer in setup.js
724
- let client: CoveoAnalyticsClient;
725
- const forcedUUID: string = 'c0b48880-743e-484f-8044-d7c37910c55b';
726
-
727
- function navigateTo(url: string) {
728
- // @ts-ignore
729
- delete window.location;
730
- // @ts-ignore
731
- window.location = new URL(url);
732
- }
733
-
734
- beforeEach(() => {
735
- client = new CoveoAnalyticsClient({});
736
- // need to clear existing clientIds
737
- client.clear();
738
- jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => false);
739
- });
740
-
741
- it('will extract a clientId from a query param if the referrer matches all and it is not expired', async () => {
742
- client.setAcceptedLinkReferrers(['*']);
743
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
744
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
745
- expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
746
- });
747
-
748
- it('will extract a clientId from a query param if the referrer matches the current referrer exactly and it is not expired', async () => {
749
- client.setAcceptedLinkReferrers(['somewhere.over']);
750
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
751
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
752
- expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
753
- });
754
-
755
- it('will extract a clientId from a query param if the referrer matches the current referrer with wildcard and it is not expired', async () => {
756
- client.setAcceptedLinkReferrers(['*.over']);
757
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
758
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
759
- expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
760
- });
670
+ describe('doNotTrack', () => {
671
+ it('should do business as usual if doNotTrack returns false', () => {
672
+ vi.spyOn(doNotTrack, 'shouldDisableAnalyticsForPrivacy').mockImplementation(() => false);
761
673
 
762
- it('will extract a clientId from a query param if one of the referrer matches the current referrer and it is not expired', async () => {
763
- client.setAcceptedLinkReferrers(['*.mydomain.com', '*.over']);
764
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
765
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
766
- expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
767
- });
674
+ let client = new CoveoAnalyticsClient({});
768
675
 
769
- it('will not extract a clientId from a query param if the referrer matches and it is expired', async () => {
770
- client.setAcceptedLinkReferrers(['*']);
771
- const linkString = new CoveoLinkParam(forcedUUID, Date.now() - 180000);
772
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
773
- expect(await client.getCurrentVisitorId()).not.toBe(null);
774
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
775
- });
676
+ expect(client.runtime).toBeInstanceOf(BrowserRuntime);
677
+ expect(client.runtime.storage).toBeInstanceOf(CookieAndLocalStorage);
678
+ });
776
679
 
777
- it('will not extract a clientId from a query param if there is no accept list specified', async () => {
778
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
779
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
780
- expect(await client.getCurrentVisitorId()).not.toBe(null);
781
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
782
- });
680
+ it('should honor doNotTrack', () => {
681
+ vi.spyOn(doNotTrack, 'shouldDisableAnalyticsForPrivacy').mockImplementation(() => true);
783
682
 
784
- it('will not extract a clientId from a query param if there is an empty accept list', async () => {
785
- client.setAcceptedLinkReferrers([]);
786
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
787
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
788
- expect(await client.getCurrentVisitorId()).not.toBe(null);
789
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
790
- });
683
+ let client = new CoveoAnalyticsClient({});
791
684
 
792
- it('will not extract a clientId from a query param if the referrer list does not match', async () => {
793
- client.setAcceptedLinkReferrers(['*.mydomain.com']);
794
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
795
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
796
- expect(await client.getCurrentVisitorId()).not.toBe(null);
797
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
798
- });
685
+ expect(client.runtime).toBeInstanceOf(NoopRuntime);
686
+ expect(client.runtime.storage).toBeInstanceOf(NullStorage);
687
+ });
799
688
 
800
- it('will not extract a clientId from a query param if the referrer list does not match the exact port', async () => {
801
- client.setAcceptedLinkReferrers(['*.over:9000']);
802
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
803
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
804
- expect(await client.getCurrentVisitorId()).not.toBe(null);
805
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
806
- });
689
+ it('should not clear existing cookies', async () => {
690
+ vi.spyOn(doNotTrack, 'shouldDisableAnalyticsForPrivacy').mockImplementation(() => true);
691
+ Cookie.set('coveo_visitorId', aVisitorId);
692
+ expect(Cookie.get('coveo_visitorId')).toBe(aVisitorId);
807
693
 
808
- it('will not extract a clientId from a query param if the multi referrer list does not match', async () => {
809
- client.setAcceptedLinkReferrers(['*.mydomain.com', 'www.example.com']);
810
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
811
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
812
- expect(await client.getCurrentVisitorId()).not.toBe(null);
813
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
814
- });
694
+ new CoveoAnalyticsClient({});
815
695
 
816
- it('will not extract a clientId from a query param if it is not a UUID', async () => {
817
- client.setAcceptedLinkReferrers(['*']);
818
- navigateTo('http://my.receivingdomain.com/?cvo_cid=notauuid.' + Math.floor(Date.now() / 1000));
819
- expect(await client.getCurrentVisitorId()).not.toBe(null);
820
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
821
- });
696
+ expect(Cookie.get('coveo_visitorId')).toBe(aVisitorId);
697
+ });
698
+ });
822
699
 
823
- it('will not extract a clientId from a query param if DNT is enabled', async () => {
824
- client.setAcceptedLinkReferrers(['*']);
825
- jest.spyOn(doNotTrack, 'doNotTrack').mockImplementation(() => true);
826
- const linkString = new CoveoLinkParam(forcedUUID, Date.now());
827
- navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
828
- expect(await client.getCurrentVisitorId()).not.toBe(null);
829
- expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
830
- });
700
+ describe('custom clientId', () => {
701
+ it('allows setting of a custom clientId', async () => {
702
+ const client = new CoveoAnalyticsClient({});
703
+ client.setClientId('c7d57b22-4aa8-487a-a106-be5243885f0a');
704
+ expect(await client.getCurrentVisitorId()).toBe('c7d57b22-4aa8-487a-a106-be5243885f0a');
705
+ });
706
+
707
+ it('persists clientId in lowercase', async () => {
708
+ const client = new CoveoAnalyticsClient({});
709
+ client.setClientId('C7D57B22-4AA8-487A-A106-BE5243885F0A');
710
+ expect(await client.getCurrentVisitorId()).toBe('c7d57b22-4aa8-487a-a106-be5243885f0a');
711
+ });
712
+
713
+ it('allows setting a custom consistent clientId given a string', async () => {
714
+ const client = new CoveoAnalyticsClient({});
715
+ client.setClientId('somestring', 'testNameSpace');
716
+ //uuid v5 specific uuid generation
717
+ expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
718
+ //check for consistent ids
719
+ client.setClientId('somestring', 'testNameSpace');
720
+ expect(await client.getCurrentVisitorId()).toBe('2c356915-8223-5773-acb8-e2a34404a559');
721
+ client.setClientId('otherstring', 'testNameSpace');
722
+ expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
723
+ client.setClientId('somestring', 'otherNameSpace');
724
+ expect(await client.getCurrentVisitorId()).not.toBe('2c356915-8223-5773-acb8-e2a34404a559');
725
+ });
726
+
727
+ it('errors when not providing a namespace', async () => {
728
+ const client = new CoveoAnalyticsClient({});
729
+ expect.assertions(1);
730
+ await expect(client.setClientId('somestring')).rejects.toEqual(
731
+ Error('Cannot generate uuid client id without a specific namespace string.')
732
+ );
733
+ //uuid v5 specific uuid generation
734
+ });
735
+ });
831
736
 
832
- it('will throw when specifying invalid hosts list', async () => {
833
- //@ts-ignore
834
- expect(() => client.setAcceptedLinkReferrers('*')).toThrow('Parameter should be an array of domain strings');
835
- //@ts-ignore
836
- expect(() => client.setAcceptedLinkReferrers({})).toThrow('Parameter should be an array of domain strings');
837
- //@ts-ignore
838
- expect(() => client.setAcceptedLinkReferrers([{}])).toThrow('Parameter should be an array of domain strings');
839
- });
737
+ describe('clientId from link', () => {
738
+ // note: referrer is set as http://somewhere.over/thereferrer in setup.js
739
+ let client: CoveoAnalyticsClient;
740
+ const forcedUUID: string = 'c0b48880-743e-484f-8044-d7c37910c55b';
741
+
742
+ function navigateTo(url: string) {
743
+ // @ts-ignore
744
+ delete window.location;
745
+ // @ts-ignore
746
+ window.location = new URL(url);
747
+ }
748
+
749
+ beforeEach(() => {
750
+ client = new CoveoAnalyticsClient({});
751
+ // need to clear existing clientIds
752
+ client.clear();
753
+ vi.spyOn(doNotTrack, 'shouldDisableAnalyticsForPrivacy').mockImplementation(() => false);
754
+ });
755
+
756
+ it('will extract a clientId from a query param if the referrer matches all and it is not expired', async () => {
757
+ client.setAcceptedLinkReferrers(['*']);
758
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
759
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
760
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
761
+ });
762
+
763
+ it('will extract a clientId from a query param if the referrer matches the current referrer exactly and it is not expired', async () => {
764
+ client.setAcceptedLinkReferrers(['somewhere.over']);
765
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
766
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
767
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
768
+ });
769
+
770
+ it('will extract a clientId from a query param if the referrer matches the current referrer with wildcard and it is not expired', async () => {
771
+ client.setAcceptedLinkReferrers(['*.over']);
772
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
773
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
774
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
775
+ });
776
+
777
+ it('will extract a clientId from a query param if one of the referrer matches the current referrer and it is not expired', async () => {
778
+ client.setAcceptedLinkReferrers(['*.mydomain.com', '*.over']);
779
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
780
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
781
+ expect(await client.getCurrentVisitorId()).toBe(forcedUUID);
782
+ });
783
+
784
+ it('will not extract a clientId from a query param if the referrer matches and it is expired', async () => {
785
+ client.setAcceptedLinkReferrers(['*']);
786
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now() - 180000);
787
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
788
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
789
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
790
+ });
791
+
792
+ it('will not extract a clientId from a query param if there is no accept list specified', async () => {
793
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
794
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
795
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
796
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
797
+ });
798
+
799
+ it('will not extract a clientId from a query param if there is an empty accept list', async () => {
800
+ client.setAcceptedLinkReferrers([]);
801
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
802
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
803
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
804
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
805
+ });
806
+
807
+ it('will not extract a clientId from a query param if the referrer list does not match', async () => {
808
+ client.setAcceptedLinkReferrers(['*.mydomain.com']);
809
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
810
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
811
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
812
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
813
+ });
814
+
815
+ it('will not extract a clientId from a query param if the referrer list does not match the exact port', async () => {
816
+ client.setAcceptedLinkReferrers(['*.over:9000']);
817
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
818
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
819
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
820
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
821
+ });
822
+
823
+ it('will not extract a clientId from a query param if the multi referrer list does not match', async () => {
824
+ client.setAcceptedLinkReferrers(['*.mydomain.com', 'www.example.com']);
825
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
826
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
827
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
828
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
829
+ });
830
+
831
+ it('will not extract a clientId from a query param if it is not a UUID', async () => {
832
+ client.setAcceptedLinkReferrers(['*']);
833
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=notauuid.' + Math.floor(Date.now() / 1000));
834
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
835
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
836
+ });
837
+
838
+ it('will not extract a clientId from a query param if DNT is enabled', async () => {
839
+ client.setAcceptedLinkReferrers(['*']);
840
+ vi.spyOn(doNotTrack, 'shouldDisableAnalyticsForPrivacy').mockImplementation(() => true);
841
+ const linkString = new CoveoLinkParam(forcedUUID, Date.now());
842
+ navigateTo('http://my.receivingdomain.com/?cvo_cid=' + linkString.toString());
843
+ expect(await client.getCurrentVisitorId()).not.toBe(null);
844
+ expect(await client.getCurrentVisitorId()).toBe(aVisitorId);
845
+ });
846
+
847
+ it('will throw when specifying invalid hosts list', async () => {
848
+ //@ts-ignore
849
+ expect(() => client.setAcceptedLinkReferrers('*')).toThrow(
850
+ 'Parameter should be an array of domain strings'
851
+ );
852
+ //@ts-ignore
853
+ expect(() => client.setAcceptedLinkReferrers({})).toThrow(
854
+ 'Parameter should be an array of domain strings'
855
+ );
856
+ //@ts-ignore
857
+ expect(() => client.setAcceptedLinkReferrers([{}])).toThrow(
858
+ 'Parameter should be an array of domain strings'
859
+ );
860
+ });
840
861
  });