Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d751d0b
updated coveo analytics to include citations attach events
SimonMilord Dec 15, 2025
20cbe2a
added analytics actions for attach citation document
SimonMilord Dec 15, 2025
c8edeb0
added redux action and modified state and slice to support attaching …
SimonMilord Dec 15, 2025
a94b567
added slice test and some improvements
SimonMilord Dec 16, 2025
3eb0bfa
applied feedback
SimonMilord Dec 16, 2025
68bd4c5
Merge branch 'main' into SFINT-6550
SimonMilord Dec 16, 2025
4b9d451
reverted changes to redux actions, no longer doing those.
SimonMilord Dec 17, 2025
f669665
Merge branch 'SFINT-6550' of https://github.com/coveo/ui-kit into SFI…
SimonMilord Dec 17, 2025
8553215
applied feedback
SimonMilord Dec 17, 2025
abbf38a
Merge branch 'main' into SFINT-6550
SimonMilord Dec 17, 2025
54169f0
lockfile update, dedupe
SimonMilord Dec 17, 2025
a4b590e
Merge branch 'SFINT-6550' of https://github.com/coveo/ui-kit into SFI…
SimonMilord Dec 17, 2025
e11572a
Merge branch 'main' into SFINT-6550
SimonMilord Dec 17, 2025
ba2dd1f
changed the param of the analytics from result to citation
SimonMilord Dec 18, 2025
ff42126
Merge branch 'SFINT-6550' of https://github.com/coveo/ui-kit into SFI…
SimonMilord Dec 18, 2025
56136ef
improved the analytic
SimonMilord Dec 18, 2025
41ce274
added new detach action for citations
SimonMilord Dec 18, 2025
bcd9613
Merge branch 'main' into SFINT-6550
SimonMilord Dec 18, 2025
f7b657e
some feedback applied
SimonMilord Dec 19, 2025
2f3a605
Merge branch 'SFINT-6550' of https://github.com/coveo/ui-kit into SFI…
SimonMilord Dec 19, 2025
a136a9e
fixed the urihash sent to analytics
SimonMilord Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/headless/src/features/analytics/analytics-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,3 +787,18 @@ export const analyticsEventItemMetadata = (
url: information.documentUri,
};
};

export const analyticsEventItemMetadataForCitations = (
citation: GeneratedAnswerCitation,
state: Partial<SearchAppState>
): ItemMetaData => {
const identifier = citationDocumentIdentifier(citation);
const information = partialCitationInformation(citation, state);
return {
uniqueFieldName: identifier.contentIdKey,
uniqueFieldValue: identifier.contentIdValue,
title: information.documentTitle,
author: information.documentAuthor,
url: information.documentUri,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export enum SearchPageEvents {
* Identifies the custom event that gets logged when a user clicks the Detach From Case result action.
*/
caseDetach = 'caseDetach',
/**
* Identifies the click event that gets logged when a user attaches a generated answer citation to a case.
*/
generatedAnswerCitationDocumentAttach = 'generatedAnswerCitationDocumentAttach',
/**
* Identifies the cause of a search request being retried in order to regenerate an answer stream that failed.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`attached results analytics actions > when analyticsMode is \`next\` > logCaseAttach > should call relay.emit properly 1`] = `
exports[`attached results analytics actions > when analyticsMode is \`next\` > #logCaseAttach > should call relay.emit properly 1`] = `
[
"InsightPanel.ItemAction",
{
Expand All @@ -23,7 +23,7 @@ exports[`attached results analytics actions > when analyticsMode is \`next\` > l
]
`;

exports[`attached results analytics actions > when analyticsMode is \`next\` > logCaseDetach > should call relay.emit properly 1`] = `
exports[`attached results analytics actions > when analyticsMode is \`next\` > #logCaseDetach > should call relay.emit properly 1`] = `
[
"InsightPanel.DetachItem",
{
Expand All @@ -42,3 +42,23 @@ exports[`attached results analytics actions > when analyticsMode is \`next\` > l
},
]
`;

exports[`attached results analytics actions > when analyticsMode is \`next\` > #logCitationDocumentDetach > should call relay.emit properly 1`] = `
[
"InsightPanel.DetachItem",
{
"context": {
"caseNumber": "5678",
"targetId": "1234",
"targetType": "Case",
},
"itemMetadata": {
"author": undefined,
"title": "example documentTitle",
"uniqueFieldName": "permanentid",
"uniqueFieldValue": "example contentIDValue",
"url": "example documentUri",
},
},
]
`;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const attachedResultPayloadDefinition = {
source: nonEmptyString,
title: requiredNonEmptyString,
uriHash: nonEmptyString,
isAttachedFromCitation: new BooleanValue({required: false, default: false}),
};

const RequiredAttachedResultRecord = new RecordValue({
Expand All @@ -48,7 +49,7 @@ const RequiredAttachedResultRecord = new RecordValue({
});

export const setAttachedResults = createAction(
'insight/attachToCase/setAttachedResults',
'insight/attachedResults/setAttachedResults',
(payload: SetAttachedResultsActionCreatorPayload) =>
validatePayload(payload, {
results: new ArrayValue({
Expand All @@ -62,12 +63,12 @@ export const setAttachedResults = createAction(
);

export const attachResult = createAction(
'insight/attachToCase/attach',
'insight/attachedResults/attach',
(payload: AttachedResult) => validatePayloadAndPermanentIdOrUriHash(payload)
);

export const detachResult = createAction(
'insight/attachToCase/detach',
'insight/attachedResults/detach',
(payload: AttachedResult) => validatePayloadAndPermanentIdOrUriHash(payload)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ import {buildMockSearchResponse} from '../../test/mock-search-response.js';
import {buildMockSearchState} from '../../test/mock-search-state.js';
import {clearMicrotaskQueue} from '../../test/unit-test-utils.js';
import {getConfigurationInitialState} from '../configuration/configuration-state.js';
import {getGeneratedAnswerInitialState} from '../generated-answer/generated-answer-state.js';
import {
logCaseAttach,
logCaseDetach,
logCitationDocumentAttach,
logCitationDocumentDetach,
} from './attached-results-analytics-actions.js';

const mockLogCaseAttach = vi.fn();
const mockLogCaseDetach = vi.fn();
const mockLogGeneratedAnswerCitationDocumentAttach = vi.fn();
const emit = vi.fn();

vi.mock('@coveo/relay');
Expand All @@ -26,6 +30,8 @@ vi.mocked(CoveoInsightClient).mockImplementation(function () {
this.disable = () => {};
this.logCaseAttach = mockLogCaseAttach;
this.logCaseDetach = mockLogCaseDetach;
this.logGeneratedAnswerCitationDocumentAttach =
mockLogGeneratedAnswerCitationDocumentAttach;
});

vi.mocked(createRelay).mockReturnValue({
Expand Down Expand Up @@ -64,6 +70,15 @@ const expectedDocumentInfo = {
documentAuthor: 'example author',
};

const expectedCitationDocumentInfo = {
queryPipeline: '',
documentUri: 'example documentUri',
sourceName: 'example sourceName',
documentPosition: 1,
documentTitle: 'example documentTitle',
documentUrl: 'example documentUrl',
};

const expectedDocumentIdentifier = {
contentIDKey: 'permanentid',
contentIDValue: 'example contentIDValue',
Expand All @@ -90,14 +105,36 @@ const resultParams = {

const testResult = buildMockResult(resultParams);

const testCitation = {
id: 'citation-123',
permanentid: 'example contentIDValue',
title: 'example documentTitle',
uri: 'example documentUri',
clickUri: 'example documentUrl',
text: 'example citation text',
fields: {
urihash: 'example documentUriHash',
source: 'example sourceName',
permanentid: 'example contentIDValue',
author: 'example author',
},
source: 'example sourceName',
};

describe('attached results analytics actions', () => {
let engine: InsightEngine;
const searchState = buildMockSearchState({
results: [testResult],
response: buildMockSearchResponse({
searchUid: 'example searchUid',
extendedResults: {
generativeQuestionAnsweringId: 'example-answer-id',
},
}),
});
const generatedAnswerState = {
...getGeneratedAnswerInitialState(),
};
const caseContextState = {
caseContext: {
Case_Subject: exampleSubject,
Expand All @@ -123,12 +160,13 @@ describe('attached results analytics actions', () => {
analyticsMode: 'legacy',
},
},
generatedAnswer: generatedAnswerState,
insightCaseContext: caseContextState,
})
);
});

describe('logCaseAttach', () => {
describe('#logCaseAttach', () => {
it('should call coveo.analytics.logCaseAttach properly', async () => {
await logCaseAttach(testResult)()(
engine.dispatch,
Expand All @@ -149,7 +187,7 @@ describe('attached results analytics actions', () => {
});
});

describe('logCaseDetach', () => {
describe('#logCaseDetach', () => {
it('should call coveo.analytics.logCaseDetach properly', async () => {
await logCaseDetach(testResult)()(
engine.dispatch,
Expand All @@ -166,6 +204,54 @@ describe('attached results analytics actions', () => {
);
});
});

describe('#logCitationDocumentAttach', () => {
it('should call coveo.analytics.logGeneratedAnswerCitationDocumentAttach properly', async () => {
await logCitationDocumentAttach(testCitation)()(
engine.dispatch,
() => engine.state,
{} as ThunkExtraArguments
);

expect(
mockLogGeneratedAnswerCitationDocumentAttach
).toHaveBeenCalledTimes(1);
expect(
mockLogGeneratedAnswerCitationDocumentAttach.mock.calls[0][0]
).toStrictEqual(expectedCitationDocumentInfo);
expect(
mockLogGeneratedAnswerCitationDocumentAttach.mock.calls[0][1]
).toStrictEqual({
generativeQuestionAnsweringId: 'example-answer-id',
citationId: testCitation.id,
documentId: {
contentIdKey: 'permanentid',
contentIdValue: testCitation.permanentid,
},
});
expect(
mockLogGeneratedAnswerCitationDocumentAttach.mock.calls[0][2]
).toStrictEqual(expectedMetadata);
});
});

describe('#logCitationDocumentDetach', () => {
it('should call coveo.analytics.logCaseDetach properly', async () => {
await logCitationDocumentDetach(testCitation)()(
engine.dispatch,
() => engine.state,
{} as ThunkExtraArguments
);

expect(mockLogCaseDetach).toHaveBeenCalledTimes(1);
expect(mockLogCaseDetach.mock.calls[0][0]).toStrictEqual(
testCitation.fields.urihash
);
expect(mockLogCaseDetach.mock.calls[0][1]).toStrictEqual(
expectedMetadata
);
});
});
});

describe('when analyticsMode is `next`', () => {
Expand All @@ -180,12 +266,13 @@ describe('attached results analytics actions', () => {
analyticsMode: 'next',
},
},
generatedAnswer: generatedAnswerState,
insightCaseContext: caseContextState,
})
);
});

describe('logCaseAttach', () => {
describe('#logCaseAttach', () => {
it('should call relay.emit properly', async () => {
await logCaseAttach(testResult)()(
engine.dispatch,
Expand All @@ -199,7 +286,7 @@ describe('attached results analytics actions', () => {
});
});

describe('logCaseDetach', () => {
describe('#logCaseDetach', () => {
it('should call relay.emit properly', async () => {
await logCaseDetach(testResult)()(
engine.dispatch,
Expand All @@ -213,5 +300,19 @@ describe('attached results analytics actions', () => {
expect(emit.mock.calls[0]).toMatchSnapshot();
});
});

describe('#logCitationDocumentDetach', () => {
it('should call relay.emit properly', async () => {
await logCitationDocumentDetach(testCitation)()(
engine.dispatch,
() => engine.state,
{} as ThunkExtraArguments
);
await clearMicrotaskQueue();

expect(emit).toHaveBeenCalledTimes(1);
expect(emit.mock.calls[0]).toMatchSnapshot();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import type {InsightPanel} from '@coveo/relay-event-types';
import type {Result} from '../../api/search/search/result.js';
import type {GeneratedAnswerCitation} from '../../index.js';
import {
analyticsEventItemMetadata,
analyticsEventItemMetadataForCitations,
citationDocumentIdentifier,
documentIdentifier,
makeInsightAnalyticsActionFactory,
partialCitationInformation,
partialDocumentInformation,
validateResultPayload,
} from '../analytics/analytics-utils.js';
import {analyticsEventCaseContext} from '../analytics/insight-analytics-utils.js';
import {SearchPageEvents} from '../analytics/search-action-cause.js';
import {getCaseContextAnalyticsMetadata} from '../case-context/case-context-state.js';
import {generativeQuestionAnsweringIdSelector} from '../generated-answer/generated-answer-selectors.js';

export const logCaseAttach = (result: Result) =>
makeInsightAnalyticsActionFactory(SearchPageEvents.caseAttach)({
Expand Down Expand Up @@ -55,3 +60,51 @@ export const logCaseDetach = (result: Result) =>
};
},
});

export const logCitationDocumentAttach = (citation: GeneratedAnswerCitation) =>
makeInsightAnalyticsActionFactory(
SearchPageEvents.generatedAnswerCitationDocumentAttach
)({
prefix: 'insight/generatedAnswerCitationDocumentAttach',
__legacy__getBuilder: (client, state) => {
const metadata = getCaseContextAnalyticsMetadata(
state.insightCaseContext
);
const generativeQuestionAnsweringId =
generativeQuestionAnsweringIdSelector(state);

if (!generativeQuestionAnsweringId || !citation) {
return null;
}

const citationPayload = {
generativeQuestionAnsweringId,
citationId: citation.id,
documentId: citationDocumentIdentifier(citation),
};
return client.logGeneratedAnswerCitationDocumentAttach(
partialCitationInformation(citation, state),
citationPayload,
metadata
);
},
});

export const logCitationDocumentDetach = (citation: GeneratedAnswerCitation) =>
makeInsightAnalyticsActionFactory(SearchPageEvents.caseDetach)({
prefix: 'insight/caseDetach',
__legacy__getBuilder: (client, state) => {
const uriHash = citation.fields?.urihash || '';
return client.logCaseDetach(
uriHash,
getCaseContextAnalyticsMetadata(state.insightCaseContext)
);
},
Comment on lines +96 to +102
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__legacy__getBuilder: (client, state) => {
const uriHash = citation.fields?.urihash || '';
return client.logCaseDetach(
uriHash,
getCaseContextAnalyticsMetadata(state.insightCaseContext)
);
},
__legacy__getBuilder: (client, state) => {
const uriHash = citation.fields?.urihash || '';
return client.logCaseDetach(
uriHash,
citation.permanentId,
getCaseContextAnalyticsMetadata(state.insightCaseContext)
);
},

we need to add this here once the coveo UA is updated

analyticsType: 'InsightPanel.DetachItem',
analyticsPayloadBuilder: (state): InsightPanel.DetachItem => {
return {
itemMetadata: analyticsEventItemMetadataForCitations(citation, state),
context: analyticsEventCaseContext(state),
};
},
});
Loading
Loading