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