feat: track copilot events (#13342)
This commit is contained in:
@@ -1271,7 +1271,7 @@ export default {
|
||||
key="copilot-bottom-panel"
|
||||
:is-generating-content="copilot.isButtonDisabled.value"
|
||||
@submit="onSubmitCopilotReply"
|
||||
@cancel="copilot.toggleEditor"
|
||||
@cancel="copilot.reset"
|
||||
/>
|
||||
<ReplyBottomPanel
|
||||
v-else
|
||||
|
||||
@@ -13,7 +13,7 @@ import { mapGetters } from 'vuex';
|
||||
// utils & constants
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { OPEN_AI_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { CAPTAIN_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
name: 'LabelSuggestion',
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
|
||||
// dismiss this once the values are set
|
||||
this.isDismissed = true;
|
||||
this.trackLabelEvent(OPEN_AI_EVENTS.DISMISS_LABEL_SUGGESTION);
|
||||
this.trackLabelEvent(CAPTAIN_EVENTS.LABEL_SUGGESTION_DISMISSED);
|
||||
},
|
||||
isConversationDismissed() {
|
||||
return LocalStorage.getFlag(
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
conversationId: this.conversationId,
|
||||
labels: labelsToAdd,
|
||||
});
|
||||
this.trackLabelEvent(OPEN_AI_EVENTS.APPLY_LABEL_SUGGESTION);
|
||||
this.trackLabelEvent(CAPTAIN_EVENTS.LABEL_SUGGESTION_APPLIED);
|
||||
},
|
||||
trackLabelEvent(event) {
|
||||
const payload = {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import TasksAPI from 'dashboard/api/captain/tasks';
|
||||
import analyticsHelper from 'dashboard/helper/AnalyticsHelper/index';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useAccount');
|
||||
@@ -23,8 +22,8 @@ vi.mock('dashboard/helper/AnalyticsHelper/index', async importOriginal => {
|
||||
return actual;
|
||||
});
|
||||
vi.mock('dashboard/helper/AnalyticsHelper/events', () => ({
|
||||
OPEN_AI_EVENTS: {
|
||||
TEST_EVENT: 'open_ai_test_event',
|
||||
CAPTAIN_EVENTS: {
|
||||
TEST_EVENT: 'captain_test_event',
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -65,17 +64,6 @@ describe('useCaptain', () => {
|
||||
expect(draftMessage.value).toBe('Draft message');
|
||||
});
|
||||
|
||||
it('records analytics correctly', async () => {
|
||||
const { recordAnalytics } = useCaptain();
|
||||
|
||||
await recordAnalytics('TEST_EVENT', { data: 'test' });
|
||||
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('open_ai_test_event', {
|
||||
type: 'TEST_EVENT',
|
||||
data: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites content', async () => {
|
||||
TasksAPI.rewrite.mockResolvedValue({
|
||||
data: { message: 'Rewritten content', follow_up_context: { id: 'ctx1' } },
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import TasksAPI from 'dashboard/api/captain/tasks';
|
||||
|
||||
export function useCaptain() {
|
||||
@@ -79,23 +78,6 @@ export function useCaptain() {
|
||||
useAlert(errorMessage);
|
||||
};
|
||||
|
||||
// === Analytics ===
|
||||
/**
|
||||
* Records analytics for AI-related events.
|
||||
* @param {string} type - The type of event.
|
||||
* @param {Object} payload - Additional data for the event.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const recordAnalytics = async (type, payload) => {
|
||||
const event = OPEN_AI_EVENTS[type.toUpperCase()];
|
||||
if (event) {
|
||||
useTrack(event, {
|
||||
type,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// === Task Methods ===
|
||||
/**
|
||||
* Rewrites content with a specific operation.
|
||||
@@ -234,8 +216,5 @@ export function useCaptain() {
|
||||
getReplySuggestion,
|
||||
followUp,
|
||||
processEvent,
|
||||
|
||||
// Analytics
|
||||
recordAnalytics,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
// Actions that map to REWRITE events (with operation attribute)
|
||||
const REWRITE_ACTIONS = [
|
||||
'improve',
|
||||
'fix_spelling_grammar',
|
||||
'casual',
|
||||
'professional',
|
||||
'expand',
|
||||
'shorten',
|
||||
'rephrase',
|
||||
'make_friendly',
|
||||
'make_formal',
|
||||
'simplify',
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the event key suffix based on action type.
|
||||
* @param {string} action - The action type
|
||||
* @returns {string} The event key prefix (REWRITE, SUMMARIZE, or REPLY_SUGGESTION)
|
||||
*/
|
||||
function getEventPrefix(action) {
|
||||
if (action === 'summarize') return 'SUMMARIZE';
|
||||
if (action === 'reply_suggestion') return 'REPLY_SUGGESTION';
|
||||
return 'REWRITE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the analytics payload based on action type.
|
||||
* @param {string} action - The action type
|
||||
* @param {number} conversationId - The conversation ID
|
||||
* @param {number} [followUpCount] - Optional follow-up count
|
||||
* @returns {Object} The payload object
|
||||
*/
|
||||
function buildPayload(action, conversationId, followUpCount = undefined) {
|
||||
const payload = { conversationId };
|
||||
|
||||
// Add operation for rewrite actions
|
||||
if (REWRITE_ACTIONS.includes(action)) {
|
||||
payload.operation = action;
|
||||
}
|
||||
|
||||
// Add followUpCount if provided
|
||||
if (followUpCount !== undefined) {
|
||||
payload.followUpCount = followUpCount;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing Copilot reply generation state and actions.
|
||||
@@ -9,7 +59,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
* @returns {Object} Copilot reply state and methods
|
||||
*/
|
||||
export function useCopilotReply() {
|
||||
const { processEvent, followUp } = useCaptain();
|
||||
const { processEvent, followUp, currentChat } = useCaptain();
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
const showEditor = ref(false);
|
||||
@@ -19,6 +69,13 @@ export function useCopilotReply() {
|
||||
const followUpContext = ref(null);
|
||||
const abortController = ref(null);
|
||||
|
||||
// Tracking state
|
||||
const currentAction = ref(null);
|
||||
const followUpCount = ref(0);
|
||||
const trackedConversationId = ref(null);
|
||||
|
||||
const conversationId = computed(() => currentChat.value?.id);
|
||||
|
||||
const isActive = computed(() => showEditor.value || isGenerating.value);
|
||||
const isButtonDisabled = computed(
|
||||
() => isGenerating.value || !isContentReady.value
|
||||
@@ -29,8 +86,22 @@ export function useCopilotReply() {
|
||||
|
||||
/**
|
||||
* Resets all copilot editor state and cancels any ongoing generation.
|
||||
* @param {boolean} [trackDismiss=true] - Whether to track dismiss event
|
||||
*/
|
||||
function reset() {
|
||||
function reset(trackDismiss = true) {
|
||||
// Track dismiss event if there was content and we're not accepting
|
||||
if (trackDismiss && generatedContent.value && currentAction.value) {
|
||||
const eventKey = `${getEventPrefix(currentAction.value)}_DISMISSED`;
|
||||
useTrack(
|
||||
CAPTAIN_EVENTS[eventKey],
|
||||
buildPayload(
|
||||
currentAction.value,
|
||||
trackedConversationId.value,
|
||||
followUpCount.value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (abortController.value) {
|
||||
abortController.value.abort();
|
||||
abortController.value = null;
|
||||
@@ -40,6 +111,9 @@ export function useCopilotReply() {
|
||||
isContentReady.value = false;
|
||||
generatedContent.value = '';
|
||||
followUpContext.value = null;
|
||||
currentAction.value = null;
|
||||
followUpCount.value = 0;
|
||||
trackedConversationId.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,11 +144,14 @@ export function useCopilotReply() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset and start new generation
|
||||
reset();
|
||||
// Reset without tracking dismiss (starting new action)
|
||||
reset(false);
|
||||
abortController.value = new AbortController();
|
||||
isGenerating.value = true;
|
||||
isContentReady.value = false;
|
||||
currentAction.value = action;
|
||||
followUpCount.value = 0;
|
||||
trackedConversationId.value = conversationId.value;
|
||||
|
||||
try {
|
||||
const { message: content, followUpContext: newContext } =
|
||||
@@ -85,7 +162,15 @@ export function useCopilotReply() {
|
||||
if (!abortController.value?.signal.aborted) {
|
||||
generatedContent.value = content;
|
||||
followUpContext.value = newContext;
|
||||
if (content) showEditor.value = true;
|
||||
if (content) {
|
||||
showEditor.value = true;
|
||||
// Track "Used" event on successful generation
|
||||
const eventKey = `${getEventPrefix(action)}_USED`;
|
||||
useTrack(
|
||||
CAPTAIN_EVENTS[eventKey],
|
||||
buildPayload(action, trackedConversationId.value)
|
||||
);
|
||||
}
|
||||
isGenerating.value = false;
|
||||
}
|
||||
} catch {
|
||||
@@ -106,6 +191,12 @@ export function useCopilotReply() {
|
||||
isGenerating.value = true;
|
||||
isContentReady.value = false;
|
||||
|
||||
// Track follow-up sent event
|
||||
useTrack(CAPTAIN_EVENTS.FOLLOW_UP_SENT, {
|
||||
conversationId: trackedConversationId.value,
|
||||
});
|
||||
followUpCount.value += 1;
|
||||
|
||||
try {
|
||||
const { message: content, followUpContext: updatedContext } =
|
||||
await followUp({
|
||||
@@ -137,7 +228,28 @@ export function useCopilotReply() {
|
||||
*/
|
||||
function accept() {
|
||||
const content = generatedContent.value;
|
||||
|
||||
// Track "Applied" event
|
||||
if (currentAction.value) {
|
||||
const eventKey = `${getEventPrefix(currentAction.value)}_APPLIED`;
|
||||
useTrack(
|
||||
CAPTAIN_EVENTS[eventKey],
|
||||
buildPayload(
|
||||
currentAction.value,
|
||||
trackedConversationId.value,
|
||||
followUpCount.value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Reset state without tracking dismiss
|
||||
showEditor.value = false;
|
||||
generatedContent.value = '';
|
||||
followUpContext.value = null;
|
||||
currentAction.value = null;
|
||||
followUpCount.value = 0;
|
||||
trackedConversationId.value = null;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,22 +84,28 @@ export const PORTALS_EVENTS = Object.freeze({
|
||||
PREVIEW_ARTICLE: 'Previewed article',
|
||||
});
|
||||
|
||||
export const OPEN_AI_EVENTS = Object.freeze({
|
||||
SUMMARIZE: 'OpenAI: Used summarize',
|
||||
REPLY_SUGGESTION: 'OpenAI: Used reply suggestion',
|
||||
REPHRASE: 'OpenAI: Used rephrase',
|
||||
IMPROVE: 'OpenAI: Used improve',
|
||||
FIX_SPELLING_AND_GRAMMAR: 'OpenAI: Used fix spelling and grammar',
|
||||
SHORTEN: 'OpenAI: Used shorten',
|
||||
EXPAND: 'OpenAI: Used expand',
|
||||
MAKE_FRIENDLY: 'OpenAI: Used make friendly',
|
||||
MAKE_FORMAL: 'OpenAI: Used make formal',
|
||||
SIMPLIFY: 'OpenAI: Used simplify',
|
||||
APPLY_LABEL_SUGGESTION: 'OpenAI: Apply label from suggestion',
|
||||
DISMISS_LABEL_SUGGESTION: 'OpenAI: Dismiss label suggestions',
|
||||
ADDED_AI_INTEGRATION_VIA_CTA_BUTTON:
|
||||
'OpenAI: Added AI integration via CTA button',
|
||||
DISMISS_AI_SUGGESTION: 'OpenAI: Dismiss AI suggestions',
|
||||
export const CAPTAIN_EVENTS = Object.freeze({
|
||||
// Rewrite events (with operation attribute in payload)
|
||||
REWRITE_USED: 'Captain: Rewrite used',
|
||||
REWRITE_APPLIED: 'Captain: Rewrite applied',
|
||||
REWRITE_DISMISSED: 'Captain: Rewrite dismissed',
|
||||
|
||||
// Summarize events
|
||||
SUMMARIZE_USED: 'Captain: Summarize used',
|
||||
SUMMARIZE_APPLIED: 'Captain: Summarize applied',
|
||||
SUMMARIZE_DISMISSED: 'Captain: Summarize dismissed',
|
||||
|
||||
// Reply suggestion events
|
||||
REPLY_SUGGESTION_USED: 'Captain: Reply suggestion used',
|
||||
REPLY_SUGGESTION_APPLIED: 'Captain: Reply suggestion applied',
|
||||
REPLY_SUGGESTION_DISMISSED: 'Captain: Reply suggestion dismissed',
|
||||
|
||||
// Follow-up events
|
||||
FOLLOW_UP_SENT: 'Captain: Follow-up sent',
|
||||
|
||||
// Label suggestions
|
||||
LABEL_SUGGESTION_APPLIED: 'Captain: Label suggestion applied',
|
||||
LABEL_SUGGESTION_DISMISSED: 'Captain: Label suggestion dismissed',
|
||||
});
|
||||
|
||||
export const COPILOT_EVENTS = Object.freeze({
|
||||
|
||||
Reference in New Issue
Block a user