feat: add captain editor events (#13524)
## Description
Adds missing analytics instrumentation for the editor AI funnel so we
can measure end-to-end usage and outcome quality.
### What was added
- Captain: Editor AI menu opened
- Captain: Generation failed
- Captain: AI-assisted message sent
### Behavior covered
- Tracks AI button click + menu open from both entry points:
- top panel sparkle button
- inline editor copilot button
- Tracks generation failures (initial + follow-up stages).
- Tracks whether accepted AI content was sent as-is or edited before
send.
### Notes
- Applies to editor Captain accept/send flow
(rewrite/summarize/reply_suggestion + follow-ups).
- Does not change Copilot sidebar flow instrumentation.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
### Manual verification steps
<img width="1906" height="832" alt="image"
src="https://github.com/user-attachments/assets/f0ade43b-aa8d-41be-8ca2-20a091a81f60"
/>
<img width="828" height="280" alt="image"
src="https://github.com/user-attachments/assets/be76219e-fb61-4a6e-bff5-dc085b0a3cc9"
/>
<img width="415" height="147" alt="image"
src="https://github.com/user-attachments/assets/36802c5c-33a7-49ed-bf7e-f0b02d86dccc"
/>
<img width="2040" height="516" alt="image"
src="https://github.com/user-attachments/assets/74b95288-bc86-4312-a282-14211ae8f25c"
/>
1. Open a conversation with Captain tasks enabled.
2. Click AI button in top panel and inline editor.
3. Confirm analytics events fire for:
- AI menu opened
4. Run an AI action and force a failure scenario (or empty response
path) and confirm generation-failed event.
5. Accept AI output, then:
- send without changes -> editedBeforeSend: false
- edit then send -> editedBeforeSend: true
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
@@ -3,6 +3,10 @@ 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';
|
||||
import {
|
||||
CAPTAIN_ERROR_TYPES,
|
||||
CAPTAIN_GENERATION_FAILURE_REASONS,
|
||||
} from 'dashboard/composables/captain/constants';
|
||||
|
||||
// Actions that map to REWRITE events (with operation attribute)
|
||||
const REWRITE_ACTIONS = [
|
||||
@@ -52,6 +56,20 @@ function buildPayload(action, conversationId, followUpCount = undefined) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function trackGenerationFailure({
|
||||
action,
|
||||
conversationId,
|
||||
followUpCount = undefined,
|
||||
stage,
|
||||
reason,
|
||||
}) {
|
||||
useTrack(CAPTAIN_EVENTS.GENERATION_FAILED, {
|
||||
...buildPayload(action, conversationId, followUpCount),
|
||||
stage,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing Copilot reply generation state and actions.
|
||||
* Extracts copilot-related logic from ReplyBox for cleaner code organization.
|
||||
@@ -146,7 +164,8 @@ export function useCopilotReply() {
|
||||
|
||||
// Reset without tracking dismiss (starting new action)
|
||||
reset(false);
|
||||
abortController.value = new AbortController();
|
||||
const requestController = new AbortController();
|
||||
abortController.value = requestController;
|
||||
isGenerating.value = true;
|
||||
isContentReady.value = false;
|
||||
currentAction.value = action;
|
||||
@@ -154,28 +173,66 @@ export function useCopilotReply() {
|
||||
trackedConversationId.value = conversationId.value;
|
||||
|
||||
try {
|
||||
const { message: content, followUpContext: newContext } =
|
||||
await processEvent(action, data, {
|
||||
signal: abortController.value.signal,
|
||||
});
|
||||
const {
|
||||
message: content,
|
||||
followUpContext: newContext,
|
||||
errorType,
|
||||
} = await processEvent(action, data, {
|
||||
signal: requestController.signal,
|
||||
});
|
||||
|
||||
if (!abortController.value?.signal.aborted) {
|
||||
generatedContent.value = content;
|
||||
followUpContext.value = newContext;
|
||||
if (content) {
|
||||
showEditor.value = true;
|
||||
// Track "Used" event on successful generation
|
||||
const eventKey = `${getEventPrefix(action)}_USED`;
|
||||
useTrack(
|
||||
CAPTAIN_EVENTS[eventKey],
|
||||
buildPayload(action, trackedConversationId.value)
|
||||
);
|
||||
if (requestController.signal.aborted) return;
|
||||
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
|
||||
if (abortController.value === requestController) {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
isGenerating.value = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (!abortController.value?.signal.aborted) {
|
||||
isGenerating.value = false;
|
||||
|
||||
generatedContent.value = content;
|
||||
followUpContext.value = newContext;
|
||||
if (content) {
|
||||
showEditor.value = true;
|
||||
// Track "Used" event on successful generation
|
||||
const eventKey = `${getEventPrefix(action)}_USED`;
|
||||
useTrack(
|
||||
CAPTAIN_EVENTS[eventKey],
|
||||
buildPayload(action, trackedConversationId.value)
|
||||
);
|
||||
} else if (errorType && errorType !== CAPTAIN_ERROR_TYPES.ABORTED) {
|
||||
trackGenerationFailure({
|
||||
action,
|
||||
conversationId: trackedConversationId.value,
|
||||
stage: 'initial',
|
||||
reason: errorType,
|
||||
});
|
||||
} else {
|
||||
trackGenerationFailure({
|
||||
action,
|
||||
conversationId: trackedConversationId.value,
|
||||
stage: 'initial',
|
||||
reason: CAPTAIN_GENERATION_FAILURE_REASONS.EMPTY_RESPONSE,
|
||||
});
|
||||
}
|
||||
isGenerating.value = false;
|
||||
} catch (error) {
|
||||
if (
|
||||
requestController.signal.aborted ||
|
||||
error?.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
||||
error?.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
||||
) {
|
||||
return;
|
||||
}
|
||||
trackGenerationFailure({
|
||||
action,
|
||||
conversationId: trackedConversationId.value,
|
||||
stage: 'initial',
|
||||
reason: error?.name || CAPTAIN_GENERATION_FAILURE_REASONS.EXCEPTION,
|
||||
});
|
||||
isGenerating.value = false;
|
||||
} finally {
|
||||
if (abortController.value === requestController) {
|
||||
abortController.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +244,8 @@ export function useCopilotReply() {
|
||||
async function sendFollowUp(message) {
|
||||
if (!followUpContext.value || !message.trim()) return;
|
||||
|
||||
abortController.value = new AbortController();
|
||||
const requestController = new AbortController();
|
||||
abortController.value = requestController;
|
||||
isGenerating.value = true;
|
||||
isContentReady.value = false;
|
||||
|
||||
@@ -198,24 +256,65 @@ export function useCopilotReply() {
|
||||
followUpCount.value += 1;
|
||||
|
||||
try {
|
||||
const { message: content, followUpContext: updatedContext } =
|
||||
await followUp({
|
||||
followUpContext: followUpContext.value,
|
||||
message,
|
||||
signal: abortController.value.signal,
|
||||
});
|
||||
const {
|
||||
message: content,
|
||||
followUpContext: updatedContext,
|
||||
errorType,
|
||||
} = await followUp({
|
||||
followUpContext: followUpContext.value,
|
||||
message,
|
||||
signal: requestController.signal,
|
||||
});
|
||||
|
||||
if (!abortController.value?.signal.aborted) {
|
||||
if (content) {
|
||||
generatedContent.value = content;
|
||||
followUpContext.value = updatedContext;
|
||||
showEditor.value = true;
|
||||
if (requestController.signal.aborted) return;
|
||||
if (errorType === CAPTAIN_ERROR_TYPES.ABORTED) {
|
||||
if (abortController.value === requestController) {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
isGenerating.value = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (!abortController.value?.signal.aborted) {
|
||||
isGenerating.value = false;
|
||||
|
||||
if (content) {
|
||||
generatedContent.value = content;
|
||||
followUpContext.value = updatedContext;
|
||||
showEditor.value = true;
|
||||
} else if (errorType && errorType !== CAPTAIN_ERROR_TYPES.ABORTED) {
|
||||
trackGenerationFailure({
|
||||
action: currentAction.value,
|
||||
conversationId: trackedConversationId.value,
|
||||
followUpCount: followUpCount.value,
|
||||
stage: 'follow_up',
|
||||
reason: errorType,
|
||||
});
|
||||
} else {
|
||||
trackGenerationFailure({
|
||||
action: currentAction.value,
|
||||
conversationId: trackedConversationId.value,
|
||||
followUpCount: followUpCount.value,
|
||||
stage: 'follow_up',
|
||||
reason: CAPTAIN_GENERATION_FAILURE_REASONS.EMPTY_RESPONSE,
|
||||
});
|
||||
}
|
||||
isGenerating.value = false;
|
||||
} catch (error) {
|
||||
if (
|
||||
requestController.signal.aborted ||
|
||||
error?.name === CAPTAIN_ERROR_TYPES.ABORT_ERROR ||
|
||||
error?.name === CAPTAIN_ERROR_TYPES.CANCELED_ERROR
|
||||
) {
|
||||
return;
|
||||
}
|
||||
trackGenerationFailure({
|
||||
action: currentAction.value,
|
||||
conversationId: trackedConversationId.value,
|
||||
followUpCount: followUpCount.value,
|
||||
stage: 'follow_up',
|
||||
reason: error?.name || CAPTAIN_GENERATION_FAILURE_REASONS.EXCEPTION,
|
||||
});
|
||||
isGenerating.value = false;
|
||||
} finally {
|
||||
if (abortController.value === requestController) {
|
||||
abortController.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user