coveo.analytics 2.30.27 → 2.30.36
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/dist/browser.mjs +49 -7
- package/dist/coveoua.browser.js +1 -1
- package/dist/coveoua.browser.js.map +1 -1
- package/dist/coveoua.debug.js +49 -7
- package/dist/coveoua.debug.js.map +1 -1
- package/dist/coveoua.js +1 -1
- package/dist/coveoua.js.map +1 -1
- package/dist/definitions/client/utils.d.ts +1 -0
- package/dist/definitions/insight/insightClient.d.ts +2 -1
- package/dist/definitions/searchPage/searchPageEvents.d.ts +19 -14
- package/dist/definitions/version.d.ts +1 -1
- package/dist/library.cjs +49 -7
- package/dist/library.es.js +49 -7
- package/dist/library.js +49 -7
- package/dist/library.mjs +49 -7
- package/dist/react-native.es.js +49 -7
- package/package.json +1 -1
- package/src/client/analytics.spec.ts +9 -6
- package/src/client/analytics.ts +5 -9
- package/src/client/utils.spec.ts +113 -0
- package/src/client/utils.ts +65 -0
- package/src/insight/insightClient.spec.ts +20 -0
- package/src/insight/insightClient.ts +8 -0
- package/src/searchPage/searchPageEvents.ts +21 -14
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {truncateUrl} from './utils';
|
|
2
|
+
|
|
3
|
+
describe('utils', () => {
|
|
4
|
+
describe('truncateUrl', () => {
|
|
5
|
+
// Note: to obtain a UTF-8 encoded escape sequence, run encodeUriComponent('<value>') in your browser console or NodeJS REPL
|
|
6
|
+
const URL_PLAIN = 'http://coveo.com/this/is/a/really/long/url/that/will/be/truncated?at=some&arbitrary=point';
|
|
7
|
+
|
|
8
|
+
it.each([[8], [16], [32], [64], [128]])(
|
|
9
|
+
`truncateUrl('${URL_PLAIN}', %d) truncates to exactly that length`,
|
|
10
|
+
(limit) => {
|
|
11
|
+
expect(truncateUrl(URL_PLAIN, limit)).toBe(URL_PLAIN.substring(0, limit));
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/** Decoded: `'http://test/ ¿OKツ😅#fine'` */
|
|
16
|
+
const URL_WITH_ESCAPES = 'http://test/%20%C2%BFOK%E3%83%84%F0%9F%98%85#fine';
|
|
17
|
+
// Number of bytes in code-point: <1>< 2 > < 3 >< 4 >
|
|
18
|
+
|
|
19
|
+
it.each([[7], [22], [45], [46], [47], [48], [100]])(
|
|
20
|
+
`truncateUrl('${URL_WITH_ESCAPES}', %d) truncates to the exact limit outside of codepoints`,
|
|
21
|
+
(limit) => {
|
|
22
|
+
expect(truncateUrl(URL_WITH_ESCAPES, limit)).toBe(URL_WITH_ESCAPES.substring(0, limit));
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
it.each([
|
|
27
|
+
[12, 12],
|
|
28
|
+
[13, 12],
|
|
29
|
+
[14, 12],
|
|
30
|
+
[15, 15],
|
|
31
|
+
])(
|
|
32
|
+
`truncateUrl('${URL_WITH_ESCAPES}', %d) does not break up single-byte codepoints`,
|
|
33
|
+
(limit, expectedLength) => {
|
|
34
|
+
expect(truncateUrl(URL_WITH_ESCAPES, limit)).toBe(URL_WITH_ESCAPES.substring(0, expectedLength));
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it.each([
|
|
39
|
+
[15, 15],
|
|
40
|
+
[16, 15],
|
|
41
|
+
[17, 15],
|
|
42
|
+
[18, 15],
|
|
43
|
+
[19, 15],
|
|
44
|
+
[20, 15],
|
|
45
|
+
[21, 21],
|
|
46
|
+
])(`truncateUrl('${URL_WITH_ESCAPES}', %d) does not break up two-byte codepoints`, (limit, expectedLength) => {
|
|
47
|
+
expect(truncateUrl(URL_WITH_ESCAPES, limit)).toBe(URL_WITH_ESCAPES.substring(0, expectedLength));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
[23, 23],
|
|
52
|
+
[24, 23],
|
|
53
|
+
[25, 23],
|
|
54
|
+
[26, 23],
|
|
55
|
+
[27, 23],
|
|
56
|
+
[28, 23],
|
|
57
|
+
[29, 23],
|
|
58
|
+
[30, 23],
|
|
59
|
+
[31, 23],
|
|
60
|
+
[32, 32],
|
|
61
|
+
])(
|
|
62
|
+
`truncateUrl('${URL_WITH_ESCAPES}', %d) does not break up three-byte codepoints`,
|
|
63
|
+
(limit, expectedLength) => {
|
|
64
|
+
expect(truncateUrl(URL_WITH_ESCAPES, limit)).toBe(URL_WITH_ESCAPES.substring(0, expectedLength));
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
it.each([
|
|
69
|
+
[32, 32],
|
|
70
|
+
[33, 32],
|
|
71
|
+
[34, 32],
|
|
72
|
+
[35, 32],
|
|
73
|
+
[36, 32],
|
|
74
|
+
[37, 32],
|
|
75
|
+
[38, 32],
|
|
76
|
+
[39, 32],
|
|
77
|
+
[40, 32],
|
|
78
|
+
[41, 32],
|
|
79
|
+
[42, 32],
|
|
80
|
+
[43, 32],
|
|
81
|
+
[44, 44],
|
|
82
|
+
])(`truncateUrl('${URL_WITH_ESCAPES}', %d) does not break up four-byte codepoints`, (limit, expectedLength) => {
|
|
83
|
+
expect(truncateUrl(URL_WITH_ESCAPES, limit)).toBe(URL_WITH_ESCAPES.substring(0, expectedLength));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const URL_WITH_INVALID_ESCAPES = 'http://test/%this%is%so%invalid';
|
|
87
|
+
|
|
88
|
+
it.each([
|
|
89
|
+
[12, 12],
|
|
90
|
+
[13, 12],
|
|
91
|
+
[14, 12],
|
|
92
|
+
[15, 15],
|
|
93
|
+
[16, 16],
|
|
94
|
+
[17, 17],
|
|
95
|
+
[18, 17],
|
|
96
|
+
[19, 17],
|
|
97
|
+
[20, 20],
|
|
98
|
+
[21, 20],
|
|
99
|
+
[22, 20],
|
|
100
|
+
[23, 23],
|
|
101
|
+
[24, 23],
|
|
102
|
+
[25, 23],
|
|
103
|
+
[26, 26],
|
|
104
|
+
])(
|
|
105
|
+
`truncateUrl('${URL_WITH_INVALID_ESCAPES}', %d) only checks for percent with invalid escapes`,
|
|
106
|
+
(limit, expectedLength) => {
|
|
107
|
+
expect(truncateUrl(URL_WITH_INVALID_ESCAPES, limit)).toBe(
|
|
108
|
+
URL_WITH_INVALID_ESCAPES.substring(0, expectedLength)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/client/utils.ts
CHANGED
|
@@ -15,3 +15,68 @@ export function isObject(o: any): boolean {
|
|
|
15
15
|
export function coerceToNumber(input: any): any {
|
|
16
16
|
return typeof input === 'string' && input != '' && !Number.isNaN(+input) ? +input : input;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Can be used to both check if the first bit is set (for any utf-8 multibyte part),
|
|
21
|
+
* and to detect "following bytes" (which start with `10xx_xxxx`)
|
|
22
|
+
*/
|
|
23
|
+
const UTF8_HIGH_BIT = 0b1000_0000;
|
|
24
|
+
/** Header for a 2-byte code point: `110x_xxxx`. Can also be used as bit-mask to check for "following bytes". */
|
|
25
|
+
const UTF8_HEADER_2 = 0b1100_0000;
|
|
26
|
+
/** Header for a 3-byte code point: `1110_xxxx`. Can also be used as bit-mask to check for "header 2". */
|
|
27
|
+
const UTF8_HEADER_3 = 0b1110_0000;
|
|
28
|
+
/** Header for a 4-byte code point: `1111_0xxx`. Can also be used as bit-mask to check for "header 3". */
|
|
29
|
+
const UTF8_HEADER_4 = 0b1111_0000;
|
|
30
|
+
|
|
31
|
+
function utf8ByteCountFromFirstByte(firstByte: number): number {
|
|
32
|
+
if ((firstByte & 0b1111_1000) === UTF8_HEADER_4) {
|
|
33
|
+
return 4;
|
|
34
|
+
}
|
|
35
|
+
if ((firstByte & UTF8_HEADER_4) === UTF8_HEADER_3) {
|
|
36
|
+
return 3;
|
|
37
|
+
}
|
|
38
|
+
if ((firstByte & UTF8_HEADER_3) === UTF8_HEADER_2) {
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Truncate a URL to an arbitrary length, taking care to not break inside a percent escape or UTF-8 multibyte sequence.
|
|
46
|
+
*
|
|
47
|
+
* @param input The input to truncate to the specified limit.
|
|
48
|
+
* @param limit The limit to apply; if negative, no truncation is applied.
|
|
49
|
+
* @returns The URL, possibly truncated to a length near limit (at most 11 characters less than limit).
|
|
50
|
+
*/
|
|
51
|
+
export function truncateUrl(input: string, limit: number): string {
|
|
52
|
+
if (limit < 0 || input.length <= limit) {
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
55
|
+
// A valid escape sequence is a percent followed by 2 hexadecimal characters; check if we split one up.
|
|
56
|
+
let end = input.indexOf('%', limit - 2);
|
|
57
|
+
if (end < 0 || end > limit) {
|
|
58
|
+
end = limit;
|
|
59
|
+
} else {
|
|
60
|
+
limit = end;
|
|
61
|
+
}
|
|
62
|
+
// Check that truncating at end won't break up an UTF-8 multibyte sequence half-way,
|
|
63
|
+
// by peeking backwards to find the first byte of an UTF-8 sequence (if present).
|
|
64
|
+
while (end > 2 && input.charAt(end - 3) == '%') {
|
|
65
|
+
const peekByte = Number.parseInt(input.substring(end - 2, end), 16);
|
|
66
|
+
// Note: if parsing fails, NaN gets coerced to 0 by the bitwise and.
|
|
67
|
+
if ((peekByte & UTF8_HIGH_BIT) != UTF8_HIGH_BIT) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
end -= 3;
|
|
71
|
+
// Check if we reached the first byte by checking it is not a "follow byte": 10xx_xxxx.
|
|
72
|
+
if ((peekByte & UTF8_HEADER_2) != UTF8_HIGH_BIT) {
|
|
73
|
+
// If the full code point is there, keep it.
|
|
74
|
+
if (limit - end >= utf8ByteCountFromFirstByte(peekByte) * 3) {
|
|
75
|
+
end = limit;
|
|
76
|
+
}
|
|
77
|
+
// Otherwise, end is already set at the correct point to truncate at (the start of the multibyte sequence).
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return input.substring(0, end);
|
|
82
|
+
}
|
|
@@ -653,6 +653,14 @@ describe('InsightClient', () => {
|
|
|
653
653
|
expectMatchCustomEventPayload(InsightEvents.createArticle, exampleCreateArticleMetadata);
|
|
654
654
|
});
|
|
655
655
|
|
|
656
|
+
it('should send proper payload for #logTriggerNotify', async () => {
|
|
657
|
+
const exampleTriggerNotifyMetadata = {
|
|
658
|
+
notifications: ['foo', 'bar'],
|
|
659
|
+
};
|
|
660
|
+
await client.logTriggerNotify(exampleTriggerNotifyMetadata);
|
|
661
|
+
expectMatchCustomEventPayload(SearchPageEvents.triggerNotify, exampleTriggerNotifyMetadata);
|
|
662
|
+
});
|
|
663
|
+
|
|
656
664
|
it('should send proper payload for #generatedAnswerFeedbackSubmitV2', async () => {
|
|
657
665
|
const exampleGeneratedAnswerMetadata = {
|
|
658
666
|
generativeQuestionAnsweringId: '123',
|
|
@@ -1488,6 +1496,18 @@ describe('InsightClient', () => {
|
|
|
1488
1496
|
await client.logCreateArticle(exampleCreateArticleMetadata, baseCaseMetadata);
|
|
1489
1497
|
expectMatchCustomEventPayload(InsightEvents.createArticle, expectedMetadata);
|
|
1490
1498
|
});
|
|
1499
|
+
|
|
1500
|
+
it('should send proper payload for #logTriggerNotify', async () => {
|
|
1501
|
+
const exampleTriggerNotifyMetadata = {
|
|
1502
|
+
notifications: ['foo', 'bar'],
|
|
1503
|
+
};
|
|
1504
|
+
const expectedMetadata = {
|
|
1505
|
+
...exampleTriggerNotifyMetadata,
|
|
1506
|
+
...expectedBaseCaseMetadata,
|
|
1507
|
+
};
|
|
1508
|
+
await client.logTriggerNotify(exampleTriggerNotifyMetadata, baseCaseMetadata);
|
|
1509
|
+
expectMatchCustomEventPayload(SearchPageEvents.triggerNotify, expectedMetadata);
|
|
1510
|
+
});
|
|
1491
1511
|
});
|
|
1492
1512
|
|
|
1493
1513
|
it('should enable analytics tracking by default', () => {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SmartSnippetFeedbackReason,
|
|
20
20
|
SmartSnippetLinkMeta,
|
|
21
21
|
SmartSnippetSuggestionMeta,
|
|
22
|
+
TriggerNotifyMetadata,
|
|
22
23
|
} from '../searchPage/searchPageEvents';
|
|
23
24
|
import {
|
|
24
25
|
ExpandToFullUIMetadata,
|
|
@@ -687,6 +688,13 @@ export class CoveoInsightClient {
|
|
|
687
688
|
);
|
|
688
689
|
}
|
|
689
690
|
|
|
691
|
+
public logTriggerNotify(triggerNotifyMetadata: TriggerNotifyMetadata, metadata?: CaseMetadata) {
|
|
692
|
+
return this.logCustomEvent(
|
|
693
|
+
SearchPageEvents.triggerNotify,
|
|
694
|
+
metadata ? {...generateMetadataToSend(metadata, false), ...triggerNotifyMetadata} : triggerNotifyMetadata
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
690
698
|
private async getBaseCustomEventRequest(metadata?: Record<string, any>) {
|
|
691
699
|
return {
|
|
692
700
|
...(await this.getBaseEventRequest(metadata)),
|
|
@@ -539,41 +539,48 @@ export interface SmartSnippetDocumentIdentifier {
|
|
|
539
539
|
|
|
540
540
|
export type PartialDocumentInformation = Omit<DocumentInformation, 'actionCause' | 'searchQueryUid'>;
|
|
541
541
|
|
|
542
|
-
|
|
542
|
+
interface AnswerGeneratedWithAnswerAPI {
|
|
543
|
+
answerAPIStreamId: string;
|
|
544
|
+
generativeQuestionAnsweringId?: never;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
interface AnswerGeneratedWithSearchAPI {
|
|
548
|
+
answerAPIStreamId?: never;
|
|
543
549
|
generativeQuestionAnsweringId: string;
|
|
544
550
|
}
|
|
545
551
|
|
|
546
|
-
export
|
|
552
|
+
export type GeneratedAnswerBaseMeta = AnswerGeneratedWithAnswerAPI | AnswerGeneratedWithSearchAPI;
|
|
553
|
+
|
|
554
|
+
export type GeneratedAnswerStreamEndMeta = GeneratedAnswerBaseMeta & {
|
|
547
555
|
answerGenerated: boolean;
|
|
548
556
|
answerTextIsEmpty?: boolean;
|
|
549
|
-
}
|
|
557
|
+
};
|
|
550
558
|
|
|
551
|
-
export
|
|
552
|
-
generativeQuestionAnsweringId: string;
|
|
559
|
+
export type GeneratedAnswerCitationMeta = GeneratedAnswerBaseMeta & {
|
|
553
560
|
permanentId: string;
|
|
554
561
|
citationId: string;
|
|
555
|
-
}
|
|
562
|
+
};
|
|
556
563
|
|
|
557
564
|
export type GeneratedAnswerFeedbackReason = 'irrelevant' | 'notAccurate' | 'outOfDate' | 'harmful' | 'other';
|
|
558
565
|
|
|
559
566
|
export type GeneratedAnswerRephraseFormat = 'step' | 'bullet' | 'concise' | 'default';
|
|
560
567
|
|
|
561
|
-
export
|
|
568
|
+
export type GeneratedAnswerSourceHoverMeta = GeneratedAnswerCitationMeta & {
|
|
562
569
|
citationHoverTimeMs: number;
|
|
563
|
-
}
|
|
570
|
+
};
|
|
564
571
|
|
|
565
|
-
export
|
|
572
|
+
export type GeneratedAnswerRephraseMeta = GeneratedAnswerBaseMeta & {
|
|
566
573
|
rephraseFormat: GeneratedAnswerRephraseFormat;
|
|
567
|
-
}
|
|
574
|
+
};
|
|
568
575
|
|
|
569
|
-
export
|
|
576
|
+
export type GeneratedAnswerFeedbackMeta = GeneratedAnswerBaseMeta & {
|
|
570
577
|
reason: GeneratedAnswerFeedbackReason;
|
|
571
578
|
details?: string;
|
|
572
|
-
}
|
|
579
|
+
};
|
|
573
580
|
|
|
574
581
|
export type GeneratedAnswerFeedbackReasonOption = 'yes' | 'unknown' | 'no';
|
|
575
582
|
|
|
576
|
-
export
|
|
583
|
+
export type GeneratedAnswerFeedbackMetaV2 = GeneratedAnswerBaseMeta & {
|
|
577
584
|
helpful: boolean;
|
|
578
585
|
readable: GeneratedAnswerFeedbackReasonOption;
|
|
579
586
|
documented: GeneratedAnswerFeedbackReasonOption;
|
|
@@ -581,4 +588,4 @@ export interface GeneratedAnswerFeedbackMetaV2 extends GeneratedAnswerBaseMeta {
|
|
|
581
588
|
hallucinationFree: GeneratedAnswerFeedbackReasonOption;
|
|
582
589
|
details?: string;
|
|
583
590
|
documentUrl?: string;
|
|
584
|
-
}
|
|
591
|
+
};
|