coveo.analytics 2.25.3 → 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.
@@ -19,10 +19,19 @@ class TestPluginWithSpy extends TestPlugin {
19
19
  super({client, uuidGenerator});
20
20
  TestPluginWithSpy.spy = jest.fn();
21
21
  }
22
+
23
+ public getApi(name: string): Function | null {
24
+ switch (name) {
25
+ case 'testMethod':
26
+ return this.testMethod;
27
+ default:
28
+ return null;
29
+ }
30
+ }
31
+
22
32
  testMethod(...args: any[]) {
23
33
  TestPluginWithSpy.spy(args);
24
34
  }
25
- someProperty: string = 'foo';
26
35
  }
27
36
 
28
37
  describe('simpleanalytics', () => {
@@ -122,21 +131,24 @@ describe('simpleanalytics', () => {
122
131
  expect(fetchMock.lastUrl()).toMatch(/^https:\/\/someendpoint\.com/);
123
132
  });
124
133
 
125
- it('uses EC and SVC plugins by default', () => {
134
+ it('uses EC, SVC and Link plugins by default', () => {
126
135
  coveoua('init', 'SOME TOKEN');
127
136
  expect(() => coveoua('callPlugin', 'ec', 'nosuchfunction')).toThrow(/does not exist/);
128
137
  expect(() => coveoua('callPlugin', 'svc', 'nosuchfunction')).toThrow(/does not exist/);
138
+ expect(() => coveoua('callPlugin', 'link', 'nosuchfunction')).toThrow(/does not exist/);
129
139
  });
130
140
 
131
141
  it('can accept no plugins', () => {
132
142
  coveoua('init', 'SOME TOKEN', {plugins: []});
133
143
  expect(() => coveoua('callPlugin', 'ec', 'nosuchfunction')).toThrow(/is not required/);
134
144
  expect(() => coveoua('callPlugin', 'svc', 'nosuchfunction')).toThrow(/is not required/);
145
+ expect(() => coveoua('callPlugin', 'link', 'nosuchfunction')).toThrow(/is not required/);
135
146
  });
136
147
 
137
148
  it('can accept one plugin', () => {
138
149
  coveoua('init', 'SOME TOKEN', {plugins: ['svc']});
139
150
  expect(() => coveoua('callPlugin', 'ec', 'nosuchfunction')).toThrow(/is not required/);
151
+ expect(() => coveoua('callPlugin', 'link', 'nosuchfunction')).toThrow(/is not required/);
140
152
  expect(() => coveoua('callPlugin', 'svc', 'nosuchfunction')).toThrow(/does not exist/);
141
153
  });
142
154
 
@@ -151,7 +163,7 @@ describe('simpleanalytics', () => {
151
163
  expect(() => coveoua('initForProxy')).toThrow(`You must pass your endpoint when you call 'initForProxy'`);
152
164
  });
153
165
 
154
- it(`throw if the initForProxy receive an endpoint that's is not a string`, () => {
166
+ it(`throw if the initForProxy receive an endpoint that is not a string`, () => {
155
167
  expect(() => coveoua('initForProxy', {})).toThrow(
156
168
  `You must pass a string as the endpoint parameter when you call 'initForProxy'`
157
169
  );
@@ -404,13 +416,6 @@ describe('simpleanalytics', () => {
404
416
 
405
417
  expect(() => coveoua('callPlugin', 'svc', 'fooBarBaz')).toThrow(/does not exist/);
406
418
  });
407
-
408
- it('throws when a namespaced action is called and that this action is not a function on the plugin', () => {
409
- coveoua('provide', 'test', TestPluginWithSpy);
410
-
411
- coveoua('init', 'SOME TOKEN', {plugins: ['test']});
412
- expect(() => coveoua('callPlugin', 'test', 'someProperty')).toThrow(/is not a function/);
413
- });
414
419
  });
415
420
 
416
421
  describe('require', () => {
@@ -129,8 +129,8 @@ export class CoveoUA {
129
129
  this.plugins.require(name, {...options, client: this.client});
130
130
  }
131
131
 
132
- callPlugin(pluginName: string, fn: string, ...args: any): void {
133
- this.plugins.execute(pluginName, fn, ...args);
132
+ callPlugin(pluginName: string, fn: string, ...args: any): any {
133
+ return this.plugins.execute(pluginName, fn, ...args);
134
134
  }
135
135
 
136
136
  reset() {
@@ -7,8 +7,7 @@ type PluginWithId = {
7
7
  readonly Id: string;
8
8
  };
9
9
 
10
- export type PluginClass = typeof BasePlugin & PluginWithId;
11
-
10
+ export type PluginClass = typeof Plugin & PluginWithId;
12
11
  export const BasePluginEventTypes = {
13
12
  pageview: 'pageview',
14
13
  event: 'event',
@@ -16,9 +15,17 @@ export const BasePluginEventTypes = {
16
15
 
17
16
  export type PluginOptions = {client: AnalyticsClient; uuidGenerator?: typeof uuidv4};
18
17
 
19
- export abstract class BasePlugin {
18
+ export abstract class Plugin {
20
19
  protected client: AnalyticsClient;
21
20
  protected uuidGenerator: typeof uuidv4;
21
+ constructor({client, uuidGenerator = uuidv4}: PluginOptions) {
22
+ this.client = client;
23
+ this.uuidGenerator = uuidGenerator;
24
+ }
25
+ public abstract getApi(name: string): any;
26
+ }
27
+
28
+ export abstract class BasePlugin extends Plugin {
22
29
  protected action?: string;
23
30
  protected actionData: {[name: string]: string} = {};
24
31
  private pageViewId: string;
@@ -28,8 +35,7 @@ export abstract class BasePlugin {
28
35
  private lastReferrer: string;
29
36
 
30
37
  constructor({client, uuidGenerator = uuidv4}: PluginOptions) {
31
- this.client = client;
32
- this.uuidGenerator = uuidGenerator;
38
+ super({client, uuidGenerator});
33
39
  this.pageViewId = uuidGenerator();
34
40
  this.nextPageViewId = this.pageViewId;
35
41
  this.currentLocation = getFormattedLocation(window.location);
@@ -37,10 +43,18 @@ export abstract class BasePlugin {
37
43
 
38
44
  this.addHooks();
39
45
  }
40
-
41
46
  protected abstract addHooks(): void;
42
47
  protected abstract clearPluginData(): void;
43
48
 
49
+ public getApi(name: string): Function | null {
50
+ switch (name) {
51
+ case 'setAction':
52
+ return this.setAction;
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+
44
58
  public setAction(action: string, options?: any) {
45
59
  this.action = action;
46
60
  this.actionData = options;
@@ -59,7 +73,7 @@ export abstract class BasePlugin {
59
73
  };
60
74
  }
61
75
 
62
- public updateLocationInformation(eventType: string, payload: any) {
76
+ protected updateLocationInformation(eventType: string, payload: any) {
63
77
  this.updateLocationForNextPageView(eventType, payload);
64
78
  }
65
79
 
package/src/plugins/ec.ts CHANGED
@@ -71,6 +71,19 @@ export class ECPlugin extends BasePlugin {
71
71
  super({client, uuidGenerator});
72
72
  }
73
73
 
74
+ public getApi(name: string): Function | null {
75
+ const superCall: Function | null = super.getApi(name);
76
+ if (superCall !== null) return superCall;
77
+ switch (name) {
78
+ case 'addProduct':
79
+ return this.addProduct;
80
+ case 'addImpression':
81
+ return this.addImpression;
82
+ default:
83
+ return null;
84
+ }
85
+ }
86
+
74
87
  protected addHooks(): void {
75
88
  this.addHooksForPageView();
76
89
  this.addHooksForEvent();
@@ -0,0 +1,171 @@
1
+ import {CoveoLinkParam, LinkPlugin} from './link';
2
+ import {createAnalyticsClientMock} from '../../tests/analyticsClientMock';
3
+ import {v4 as uuidv4} from 'uuid';
4
+
5
+ function currentSecsSinceEpoch() {
6
+ return Math.floor(Date.now() / 1000);
7
+ }
8
+
9
+ describe('CoveoLinkParam class', () => {
10
+ it('can create a new link using a uuid', () => {
11
+ const uuid = uuidv4();
12
+ const link: CoveoLinkParam = new CoveoLinkParam(uuid, Date.now());
13
+ expect(link.clientId).toBe(uuid);
14
+ expect(link.creationDate).toBe(currentSecsSinceEpoch());
15
+ });
16
+
17
+ it('can create a new link using a uuid and timestamp', () => {
18
+ const uuid = uuidv4();
19
+ const link: CoveoLinkParam = new CoveoLinkParam(uuid, 1676298678329);
20
+ expect(link.clientId).toBe(uuid);
21
+ expect(link.creationDate).toBe(1676298678);
22
+ });
23
+
24
+ it('can not create a new link using a non uuid', () => {
25
+ const uuid = 'Not_a_uuid';
26
+ expect(() => new CoveoLinkParam(uuid, Date.now())).toThrow('Not a valid uuid');
27
+ });
28
+
29
+ it('can parse a link from a string', () => {
30
+ const link = 'c0b48880743e484f8044d7c37910c55b.1676298678';
31
+ const coveoLinkParam: CoveoLinkParam | null = CoveoLinkParam.fromString(link);
32
+ expect(coveoLinkParam).not.toBeNull;
33
+ expect(coveoLinkParam?.clientId).toBe('c0b48880-743e-484f-8044-d7c37910c55b');
34
+ expect(coveoLinkParam?.creationDate).toBe(1676298678);
35
+ });
36
+
37
+ it('will not parse links without a valid uuid', () => {
38
+ const link = 'a0c56830743d46537f703.1676298678';
39
+ const coveoLinkParam: CoveoLinkParam | null = CoveoLinkParam.fromString(link);
40
+ expect(coveoLinkParam).toBe(null);
41
+ });
42
+
43
+ it('will not parse links with an invalid structure', () => {
44
+ const link = 'a0c56830743d46537f703.1676298678.353673463';
45
+ const coveoLinkParam: CoveoLinkParam | null = CoveoLinkParam.fromString(link);
46
+ expect(coveoLinkParam).toBe(null);
47
+ });
48
+
49
+ it('will not parse links with invalid timestamps', () => {
50
+ const link = 'a0c56830743d46537f703.invalidtimestamp';
51
+ const coveoLinkParam: CoveoLinkParam | null = CoveoLinkParam.fromString(link);
52
+ expect(coveoLinkParam).toBe(null);
53
+ });
54
+
55
+ it('can serialize a link to a string', () => {
56
+ const coveoLink: CoveoLinkParam = new CoveoLinkParam('074af291-224b-4705-9dc5-a47bd80a8db9', Date.now());
57
+ const link = coveoLink.toString();
58
+ const parts = link.split('.');
59
+ expect(parts[0]).toBe('074af291224b47059dc5a47bd80a8db9');
60
+ expect(Number.parseInt(parts[1])).toBe(currentSecsSinceEpoch());
61
+ });
62
+
63
+ it('checks for expiration on a link', () => {
64
+ const coveoLink1: CoveoLinkParam = new CoveoLinkParam('074af291-224b-4705-9dc5-a47bd80a8db9', Date.now());
65
+ expect(coveoLink1.expired).toBe(false);
66
+ const coveoLink2: CoveoLinkParam = new CoveoLinkParam(
67
+ '074af291-224b-4705-9dc5-a47bd80a8db9',
68
+ Date.now() - 180000
69
+ );
70
+ expect(coveoLink2.expired).toBe(true);
71
+ });
72
+
73
+ it('checks for expiration on a link if the timestamp is in the future', () => {
74
+ const coveoLink1: CoveoLinkParam = new CoveoLinkParam('074af291-224b-4705-9dc5-a47bd80a8db9', Date.now());
75
+ expect(coveoLink1.expired).toBe(false);
76
+ const coveoLink2: CoveoLinkParam = new CoveoLinkParam(
77
+ '074af291-224b-4705-9dc5-a47bd80a8db9',
78
+ Date.now() + 5000
79
+ );
80
+ expect(coveoLink2.expired).toBe(true);
81
+ });
82
+
83
+ it('checks validation on referrers', () => {
84
+ const coveoLink1: CoveoLinkParam = new CoveoLinkParam('074af291-224b-4705-9dc5-a47bd80a8db9', Date.now());
85
+ expect(coveoLink1.validate('http://sub.mysite.com', ['*'])).toBe(true);
86
+ expect(coveoLink1.validate('http://sub.mysite.com', ['*.mysite.com', '*'])).toBe(true);
87
+ expect(coveoLink1.validate('http://sub.mysite.com', ['*.mysite.com'])).toBe(true);
88
+ expect(coveoLink1.validate('http://sub.notmysite.com', ['*.mysite.com'])).toBe(false);
89
+ expect(coveoLink1.validate('http://sub.mysite.com', [])).toBe(false);
90
+ });
91
+
92
+ it('escapes backslash correctly', () => {
93
+ const coveoLink1: CoveoLinkParam = new CoveoLinkParam('074af291-224b-4705-9dc5-a47bd80a8db9', Date.now());
94
+ expect(coveoLink1.validate('http://sub.mysite.com', ['\\w'])).toBe(false); // This should not be treated as a \w regexp!
95
+ });
96
+ });
97
+
98
+ describe('CoveoLinkPlugin', () => {
99
+ let link: LinkPlugin;
100
+
101
+ beforeEach(() => {
102
+ jest.clearAllMocks();
103
+ const analyticsClient = createAnalyticsClientMock();
104
+ analyticsClient.getCurrentVisitorId = jest.fn(() => Promise.resolve('85698661-efdf-4c6d-9cad-c4632bf81ce3'));
105
+ link = new LinkPlugin({client: analyticsClient});
106
+ });
107
+
108
+ it('decorates links with valid urls and no params', async () => {
109
+ const url: string = 'https://coveo.com';
110
+ const result: string = await link.decorate(url);
111
+ expect(result).toBe('https://coveo.com/?cvo_cid=85698661efdf4c6d9cadc4632bf81ce3.' + currentSecsSinceEpoch());
112
+ });
113
+
114
+ it('decorates links with valid urls and no params', async () => {
115
+ const url: string = 'https://coveo.com/some/path/';
116
+ const result: string = await link.decorate(url);
117
+ expect(result).toBe(
118
+ 'https://coveo.com/some/path/?cvo_cid=85698661efdf4c6d9cadc4632bf81ce3.' + currentSecsSinceEpoch()
119
+ );
120
+ });
121
+
122
+ it('decorates links with valid urls and existing params', async () => {
123
+ const url: string = 'https://coveo.com/query?q=something&p=param';
124
+ const result: string = await link.decorate(url);
125
+ expect(result).toBe(
126
+ 'https://coveo.com/query?q=something&p=param&cvo_cid=85698661efdf4c6d9cadc4632bf81ce3.' +
127
+ currentSecsSinceEpoch()
128
+ );
129
+ });
130
+
131
+ it('decorates links with valid urls, existing params and fragments', async () => {
132
+ const url: string = 'https://coveo.com/?q=something#frag';
133
+ const result: string = await link.decorate(url);
134
+ expect(result).toBe(
135
+ 'https://coveo.com/?q=something&cvo_cid=85698661efdf4c6d9cadc4632bf81ce3.' +
136
+ currentSecsSinceEpoch() +
137
+ '#frag'
138
+ );
139
+ });
140
+
141
+ it('updates an existing decoration links with valid urls and no params', async () => {
142
+ const url: string = 'https://coveo.com/?cvo_cid=c0b48880743e484f8044d7c37910c55b.1676298678';
143
+ const result: string = await link.decorate(url);
144
+ expect(result).toBe('https://coveo.com/?cvo_cid=85698661efdf4c6d9cadc4632bf81ce3.' + currentSecsSinceEpoch());
145
+ });
146
+
147
+ it('errors on invalid urls', async () => {
148
+ const url: string = 'somethingthatisobviouslynotaurl';
149
+ await expect(link.decorate(url)).rejects.toThrow('Invalid URL provided');
150
+ });
151
+
152
+ it('errors when there is no current clientId', async () => {
153
+ // initialize with missing clientId method
154
+ const analyticsClient = createAnalyticsClientMock();
155
+ analyticsClient.getCurrentVisitorId = undefined;
156
+ link = new LinkPlugin({client: analyticsClient});
157
+
158
+ const url: string = 'https://coveo.com/';
159
+ await expect(link.decorate(url)).rejects.toThrow('Could not retrieve current clientId');
160
+ });
161
+
162
+ it('can set a list of referrers on the client', () => {
163
+ const analyticsClient = createAnalyticsClientMock();
164
+ const mock: jest.Mock = jest.fn();
165
+ analyticsClient.setAcceptedLinkReferrers = mock;
166
+ link = new LinkPlugin({client: analyticsClient});
167
+ link.acceptFrom(['*.somedomain.com', 'www.coveo.com']);
168
+
169
+ expect(mock).toHaveBeenCalledTimes(1);
170
+ });
171
+ });
@@ -0,0 +1,109 @@
1
+ import {validate as uuidvalidate, v4 as uuidv4} from 'uuid';
2
+ import {Plugin, PluginOptions, PluginClass} from './BasePlugin';
3
+
4
+ export class CoveoLinkParam {
5
+ public static readonly cvo_cid: string = 'cvo_cid'; // name of the url parameter
6
+ private static readonly expirationTime: number = 120; // expirationTime in secs
7
+ public readonly clientId: string; //uuid
8
+ public readonly creationDate: number; //seconds since epoch to save space in serialized param
9
+
10
+ constructor(clientId: string, timestamp: number) {
11
+ if (!uuidvalidate(clientId)) throw Error('Not a valid uuid');
12
+ this.clientId = clientId;
13
+ this.creationDate = Math.floor(timestamp / 1000);
14
+ }
15
+
16
+ public toString(): string {
17
+ // strips the dashes and uses second granularity to save on url length.
18
+ return this.clientId.replace(/-/g, '') + '.' + this.creationDate.toString();
19
+ }
20
+
21
+ public get expired(): boolean {
22
+ const age = Math.floor(Date.now() / 1000) - this.creationDate;
23
+ return age < 0 || age > CoveoLinkParam.expirationTime;
24
+ }
25
+
26
+ public validate(referrerString: string, referrerList: string[]): boolean {
27
+ return !this.expired && this.matchReferrer(referrerString, referrerList);
28
+ }
29
+
30
+ private matchReferrer(referrerString: string, referrerList: string[]): boolean {
31
+ try {
32
+ const url: URL = new URL(referrerString);
33
+ return referrerList.some((value: string) => {
34
+ const hostRegExp: RegExp = new RegExp(
35
+ value.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'
36
+ );
37
+ return hostRegExp.test(url.host);
38
+ });
39
+ } catch (error) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ public static fromString(input: string): CoveoLinkParam | null {
45
+ const parts = input.split('.');
46
+ if (parts.length !== 2) {
47
+ return null;
48
+ }
49
+ const [clientIdPart, creationDate] = parts;
50
+ if (clientIdPart.length !== 32 || isNaN(parseInt(creationDate))) {
51
+ return null;
52
+ }
53
+ const clientId =
54
+ clientIdPart.substring(0, 8) +
55
+ '-' +
56
+ clientIdPart.substring(8, 12) +
57
+ '-' +
58
+ clientIdPart.substring(12, 16) +
59
+ '-' +
60
+ clientIdPart.substring(16, 20) +
61
+ '-' +
62
+ clientIdPart.substring(20, 32);
63
+ if (uuidvalidate(clientId)) {
64
+ return new CoveoLinkParam(clientId, Number.parseInt(creationDate) * 1000);
65
+ } else {
66
+ return null;
67
+ }
68
+ }
69
+ }
70
+
71
+ export class LinkPlugin extends Plugin {
72
+ public static readonly Id = 'link';
73
+
74
+ constructor({client, uuidGenerator = uuidv4}: PluginOptions) {
75
+ super({client, uuidGenerator});
76
+ }
77
+
78
+ public getApi(name: string): Function | null {
79
+ switch (name) {
80
+ case 'decorate':
81
+ return this.decorate;
82
+ case 'acceptFrom':
83
+ return this.acceptFrom;
84
+ default:
85
+ return null;
86
+ }
87
+ }
88
+
89
+ public async decorate(urlString: string): Promise<string> {
90
+ // Note: clientId retrieval function is marked as optional
91
+ if (!this.client.getCurrentVisitorId) {
92
+ throw new Error('Could not retrieve current clientId');
93
+ }
94
+ try {
95
+ const url = new URL(urlString);
96
+ const clientId: string = await this.client.getCurrentVisitorId!();
97
+ url.searchParams.set(CoveoLinkParam.cvo_cid, new CoveoLinkParam(clientId, Date.now()).toString());
98
+ return url.toString();
99
+ } catch (error) {
100
+ throw new Error('Invalid URL provided');
101
+ }
102
+ }
103
+
104
+ public acceptFrom(acceptedReferrers: string[]) {
105
+ this.client.setAcceptedLinkReferrers!(acceptedReferrers);
106
+ }
107
+ }
108
+
109
+ export const Link: PluginClass = LinkPlugin;
@@ -34,6 +34,17 @@ export class SVCPlugin extends BasePlugin {
34
34
  super({client, uuidGenerator});
35
35
  }
36
36
 
37
+ public getApi(name: string): Function | null {
38
+ const superCall: Function | null = super.getApi(name);
39
+ if (superCall !== null) return superCall;
40
+ switch (name) {
41
+ case 'setTicket':
42
+ return this.setTicket;
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+
37
48
  protected addHooks(): void {
38
49
  this.addHooksForEvent();
39
50
  this.addHooksForPageView();
@@ -1 +0,0 @@
1
- export declare const getFormattedLocation: (location: Location) => string;