feat(core): Add telemetry for the WhatsApp channel setup (no-changelog)

This commit is contained in:
Toni Devine 2026-10-16 16:51:31 +01:00
parent 0127de4f67
commit 328df635a5
No known key found for this signature in database
10 changed files with 384 additions and 7 deletions

View File

@ -225,6 +225,19 @@ export const AGENTS_TELEMETRY = defineTelemetryEvents({
),
}),
},
AGENT_WHATSAPP_WEBHOOK_VERIFIED: {
name: 'Agent WhatsApp webhook verified',
description:
'Meta completed the webhook handshake with an agent: its callback URL is reachable and the verify token matched. Shows how many WhatsApp setups get past the Meta webhook step. Meta repeats the handshake each time someone clicks Verify and save, so one agent can emit this more than once.',
properties: z.object({
agent_id: z.string(),
phase: z
.enum(['setup', 'connected'])
.describe(
'setup: the channel was not connected yet, so n8n answered without a credential; connected: the running channel answered, which is a re-verification',
),
}),
},
USER_RESPONDED_TO_AGENT_TOOL_APPROVAL: {
name: 'User responded to agent tool approval',
description:
@ -733,6 +746,58 @@ export const AGENTS_TELEMETRY = defineTelemetryEvents({
session_id: sessionId,
}),
},
USER_CHECKED_WHATSAPP_CHANNEL_CREDENTIAL: {
name: 'User checked WhatsApp channel credential',
description:
'The WhatsApp channel setup checked the selected credential against Meta. Runs on its own whenever the selected credential changes, and when the user clicks Check again. Shows why credentials fail on the way to a connected channel.',
properties: z.object({
agent_id: z.string(),
trigger: z
.enum(['auto', 'recheck'])
.describe('auto: the selected credential changed; recheck: the user clicked Check again'),
status: z.enum(['ok', 'failed']),
reason: z
.enum([
'incomplete',
'temporary_token',
'mismatch',
'unreachable',
'not_found',
'request_failed',
])
.optional()
.describe(
'Only when status is failed. not_found: the user cannot use that credential; request_failed: the request to n8n failed, so Meta was never asked',
),
session_id: sessionId,
}),
},
USER_CLICKED_WHATSAPP_CHANNEL_LINK: {
name: 'User clicked WhatsApp channel link',
description:
'The user clicked a link in the WhatsApp channel setup or settings that leaves n8n for Meta. Shows where people go to Meta, and whether they need to go back for something the setup could have told them.',
properties: z.object({
agent_id: z.string(),
target: z
.enum(['meta_dashboard', 'whatsapp_manager', 'meta_app'])
.describe(
'meta_dashboard: Open Meta App Dashboard in the setup; whatsapp_manager: Edit in WhatsApp Manager in the settings; meta_app: Find your Meta app in the settings',
),
session_id: sessionId,
}),
},
USER_SAW_WHATSAPP_REVIEW_STATE: {
name: 'User saw WhatsApp review state',
description:
"The WhatsApp channel settings showed a callout about Meta's review of the phone number. Fires when the settings open with a review open or declined, and again when the state changes, never on every poll. Shows how often Meta declines or restricts a number.",
properties: z.object({
agent_id: z.string(),
state: z
.enum(['pending_verification', 'pending_name_review', 'name_declined', 'restricted'])
.describe('Which review state the callout described'),
session_id: sessionId,
}),
},
USER_CLICKED_DEPLOY_TO_AZURE_FOR_TEAMS_CHANNEL: {
name: 'User clicked Deploy to Azure for Teams channel',
description: 'The user clicked the Deploy to Azure button in the Teams channel setup.',

View File

@ -19,6 +19,7 @@ import type {
} from '../../../integration-tools';
import type { AgentRepository } from '../../../../repositories/agent.repository';
import { WhatsAppIntegration } from '../../../platforms/whatsapp/whatsapp-integration';
import type { WhatsAppSetupTelemetryService } from '../../../platforms/whatsapp/whatsapp-setup-telemetry.service';
import type { WhatsAppWebhookVerificationStore } from '../../../platforms/whatsapp/whatsapp-webhook-verification.store';
import {
createMockAttachmentService,
@ -140,6 +141,7 @@ export function whatsAppThreadId(
export function createWhatsAppIntegration(
channelRateLimitGuard = new ChannelRateLimitGuard(),
webhookVerification: WhatsAppWebhookVerificationStore = mock<WhatsAppWebhookVerificationStore>(),
setupTelemetry: WhatsAppSetupTelemetryService = mock<WhatsAppSetupTelemetryService>(),
): WhatsAppIntegration {
return new WhatsAppIntegration(
mock<BackendLogger>(),
@ -150,6 +152,7 @@ export function createWhatsAppIntegration(
}),
channelRateLimitGuard,
webhookVerification,
setupTelemetry,
);
}
@ -289,6 +292,7 @@ export async function createWhatsAppReplayContext(
failedApiTypes?: string[];
failureSequence?: WhatsAppFailureSequence;
webhookVerification?: WhatsAppWebhookVerificationStore;
setupTelemetry?: WhatsAppSetupTelemetryService;
} = {},
): Promise<WhatsAppReplayContext> {
const stub = installWhatsAppApiStub(options.failedApiTypes, options.failureSequence);
@ -301,6 +305,7 @@ export async function createWhatsAppReplayContext(
const integrationImpl = createWhatsAppIntegration(
channelRateLimitGuard,
options.webhookVerification,
options.setupTelemetry,
);
const integration: AgentIntegrationConfig = options.integration ?? {
type: 'whatsapp',

View File

@ -28,6 +28,7 @@ import {
whatsAppReplayFixtures,
whatsAppWebhook,
} from '../../../__tests__/helpers/whatsapp/synthetic-fixtures';
import type { WhatsAppSetupTelemetryService } from '../../whatsapp/whatsapp-setup-telemetry.service';
import type { WhatsAppWebhookVerificationStore } from '../../whatsapp/whatsapp-webhook-verification.store';
// The chat SDK + adapters are ESM-only. Production loads them via esm-loader's
@ -1095,8 +1096,10 @@ describe('WhatsApp Cloud API integration scenarios', () => {
it('records the handshake when the token matches', async () => {
const webhookVerification = mock<WhatsAppWebhookVerificationStore>();
const setupTelemetry = mock<WhatsAppSetupTelemetryService>();
const ctx = await createWhatsAppReplayContext(whatsAppReplayFixtures(), {
webhookVerification,
setupTelemetry,
});
try {
@ -1105,6 +1108,10 @@ describe('WhatsApp Cloud API integration scenarios', () => {
expect(response.status).toBe(200);
expect(await response.text()).toBe('12345');
expect(webhookVerification.markVerified).toHaveBeenCalledExactlyOnceWith('agent-1');
expect(setupTelemetry.webhookVerified).toHaveBeenCalledExactlyOnceWith(
'agent-1',
'connected',
);
} finally {
await ctx.shutdown();
}
@ -1112,8 +1119,10 @@ describe('WhatsApp Cloud API integration scenarios', () => {
it('does not record a rejected handshake', async () => {
const webhookVerification = mock<WhatsAppWebhookVerificationStore>();
const setupTelemetry = mock<WhatsAppSetupTelemetryService>();
const ctx = await createWhatsAppReplayContext(whatsAppReplayFixtures(), {
webhookVerification,
setupTelemetry,
});
try {
@ -1121,6 +1130,7 @@ describe('WhatsApp Cloud API integration scenarios', () => {
expect(response.status).toBe(403);
expect(webhookVerification.markVerified).not.toHaveBeenCalled();
expect(setupTelemetry.webhookVerified).not.toHaveBeenCalled();
} finally {
await ctx.shutdown();
}
@ -1135,7 +1145,8 @@ describe('WhatsApp Cloud API integration scenarios', () => {
it("answers Meta's handshake with the raw challenge when the token matches", () => {
const webhookVerification = mock<WhatsAppWebhookVerificationStore>();
const integration = createWhatsAppIntegration(undefined, webhookVerification);
const setupTelemetry = mock<WhatsAppSetupTelemetryService>();
const integration = createWhatsAppIntegration(undefined, webhookVerification, setupTelemetry);
const result = integration.handleUnauthenticatedWebhook({
agentId: 'agent-1',
@ -1151,11 +1162,13 @@ describe('WhatsApp Cloud API integration scenarios', () => {
expect(result).toEqual({ status: 200, body: '12345', raw: true });
expect(webhookVerification.markVerified).toHaveBeenCalledExactlyOnceWith('agent-1');
expect(setupTelemetry.webhookVerified).toHaveBeenCalledExactlyOnceWith('agent-1', 'setup');
});
it('rejects the handshake when the verify token does not match', () => {
const webhookVerification = mock<WhatsAppWebhookVerificationStore>();
const integration = createWhatsAppIntegration(undefined, webhookVerification);
const setupTelemetry = mock<WhatsAppSetupTelemetryService>();
const integration = createWhatsAppIntegration(undefined, webhookVerification, setupTelemetry);
const result = integration.handleUnauthenticatedWebhook({
agentId: 'agent-1',

View File

@ -0,0 +1,33 @@
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { mock } from 'vitest-mock-extended';
import type { Telemetry } from '@/telemetry';
import { WhatsAppSetupTelemetryService } from '../../whatsapp/whatsapp-setup-telemetry.service';
describe('WhatsAppSetupTelemetryService', () => {
let telemetry: ReturnType<typeof mock<Telemetry>>;
let service: WhatsAppSetupTelemetryService;
beforeEach(() => {
telemetry = mock<Telemetry>();
service = new WhatsAppSetupTelemetryService(telemetry);
});
it.each(['setup', 'connected'] as const)('reports a %s handshake', (phase) => {
service.webhookVerified('agent-1', phase);
expect(telemetry.track).toHaveBeenCalledExactlyOnceWith(
TELEMETRY_EVENT.AGENTS.AGENT_WHATSAPP_WEBHOOK_VERIFIED,
{ agent_id: 'agent-1', phase },
);
});
it('does not throw when reporting fails, so Meta still gets its answer', () => {
telemetry.track.mockImplementation(() => {
throw new Error('transport down');
});
expect(() => service.webhookVerified('agent-1', 'setup')).not.toThrow();
});
});

View File

@ -41,6 +41,7 @@ import { deriveWhatsAppVerifyToken, stringProperty, stringValue } from '../../in
import { resolveIntegrationActionDefinitions } from '../../integration-tool-definitions';
import { buildIntegrationConnectionId } from '../../integration-tool-factory';
import { startTypingIndicator } from '../typing-indicator';
import { WhatsAppSetupTelemetryService } from './whatsapp-setup-telemetry.service';
import { WhatsAppWebhookVerificationStore } from './whatsapp-webhook-verification.store';
type ChatSdk = Awaited<ReturnType<typeof loadChatSdk>>;
@ -194,6 +195,7 @@ export class WhatsAppIntegration extends AgentChatIntegration {
private readonly instanceSettings: InstanceSettings,
private readonly channelRateLimitGuard: ChannelRateLimitGuard,
private readonly webhookVerification: WhatsAppWebhookVerificationStore,
private readonly setupTelemetry: WhatsAppSetupTelemetryService,
) {
super();
}
@ -242,6 +244,7 @@ export class WhatsAppIntegration extends AgentChatIntegration {
// Recorded so the setup can confirm the step. Not awaited, because Meta
// times out the handshake and the record is only for display.
void this.webhookVerification.markVerified(context.agentId);
this.setupTelemetry.webhookVerified(context.agentId, 'setup');
return { status: 200, body: challenge, raw: true };
}
@ -298,6 +301,7 @@ export class WhatsAppIntegration extends AgentChatIntegration {
// retry cycle against an already-limited number instead of failing fast.
const connectionId = buildIntegrationConnectionId(ctx.integration);
const webhookVerification = this.webhookVerification;
const setupTelemetry = this.setupTelemetry;
const agentId = ctx.agentId;
// Wraps the real adapter to enforce the 24-hour customer service window
@ -310,7 +314,10 @@ export class WhatsAppIntegration extends AgentChatIntegration {
// the same agent still confirms the step.
protected override handleVerificationChallenge(request: Request): Response {
const response = super.handleVerificationChallenge(request);
if (response.status === 200) void webhookVerification.markVerified(agentId);
if (response.status === 200) {
void webhookVerification.markVerified(agentId);
setupTelemetry.webhookVerified(agentId, 'connected');
}
return response;
}

View File

@ -0,0 +1,26 @@
import { Service } from '@n8n/di';
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { Telemetry } from '@/telemetry';
/**
* Reports how far a WhatsApp setup got on the n8n side. The credential step is
* reported by the editor, because only it knows when a check was a re-check.
* Meta's handshake reaches the backend directly, so it is reported here.
*/
@Service()
export class WhatsAppSetupTelemetryService {
constructor(private readonly telemetry: Telemetry) {}
/** Never throws: Meta has already received its answer when this runs. */
webhookVerified(agentId: string, phase: 'setup' | 'connected'): void {
try {
this.telemetry.track(TELEMETRY_EVENT.AGENTS.AGENT_WHATSAPP_WEBHOOK_VERIFIED, {
agent_id: agentId,
phase,
});
} catch {
// A failed report must not change the answer Meta gets.
}
}
}

View File

@ -27,7 +27,7 @@ const props = withDefaults(
},
);
const emit = defineEmits<{ connect: [] }>();
const emit = defineEmits<{ connect: []; openManager: [] }>();
/** The profile is edited in Meta, not in n8n. */
const WHATSAPP_MANAGER_URL = 'https://business.facebook.com/latest/whatsapp_manager/phone_numbers/';
@ -94,6 +94,7 @@ const subtitle = computed(() => {
variant="outline"
size="medium"
data-testid="whatsapp-manager-link"
@click="emit('openManager')"
>
{{ i18n.baseText('agents.channels.whatsapp.settings.editInManager') }}
<N8nIcon icon="arrow-up-right" size="small" />

View File

@ -8,6 +8,7 @@ import { configure, fireEvent, waitFor } from '@testing-library/vue';
import { flushPromises } from '@vue/test-utils';
import { defineComponent, ref } from 'vue';
import type { WhatsAppCredentialCheck } from '@n8n/api-types';
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import AgentChannelWhatsAppSetup from './AgentChannelWhatsAppSetup.vue';
import { ResponseError } from '@n8n/rest-api-client';
@ -34,6 +35,11 @@ vi.mock('@n8n/design-system', async (importOriginal) => ({
},
}));
const { trackMock } = vi.hoisted(() => ({ trackMock: vi.fn() }));
vi.mock('@n8n/composables/useTelemetry', () => ({
useTelemetry: () => ({ track: trackMock }),
}));
const { showMessage, showError } = vi.hoisted(() => ({ showMessage: vi.fn(), showError: vi.fn() }));
vi.mock('@n8n/composables/useToast', () => ({
useToast: () => ({ showMessage, showError }),
@ -705,6 +711,138 @@ describe('AgentChannelWhatsAppSetup', () => {
});
});
describe('telemetry', () => {
const EVENT = TELEMETRY_EVENT.AGENTS.USER_CHECKED_WHATSAPP_CHANNEL_CREDENTIAL;
it('tracks a credential that checks out', async () => {
renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() =>
expect(trackMock).toHaveBeenCalledWith(
EVENT,
expect.objectContaining({ agent_id: 'a', trigger: 'auto', status: 'ok' }),
),
);
});
it.each([
['incomplete', 'incomplete'],
['temporaryToken', 'temporary_token'],
['mismatch', 'mismatch'],
['unreachable', 'unreachable'],
] as const)('tracks a %s credential as %s', async (reason, tracked) => {
vi.mocked(checkWhatsAppCredential).mockResolvedValue({ status: 'failed', reason });
renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() =>
expect(trackMock).toHaveBeenCalledWith(
EVENT,
expect.objectContaining({ status: 'failed', reason: tracked }),
),
);
});
it('tracks a credential the caller cannot use as not found', async () => {
vi.mocked(checkWhatsAppCredential).mockRejectedValue(
new ResponseError('missing', { httpStatusCode: 404 }),
);
renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() =>
expect(trackMock).toHaveBeenCalledWith(
EVENT,
expect.objectContaining({ status: 'failed', reason: 'not_found' }),
),
);
});
it('tracks a failed request to n8n apart from Meta being unreachable', async () => {
vi.mocked(checkWhatsAppCredential).mockRejectedValue(new Error('offline'));
renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() =>
expect(trackMock).toHaveBeenCalledWith(
EVENT,
expect.objectContaining({ status: 'failed', reason: 'request_failed' }),
),
);
});
it('tracks a recheck as one the user asked for', async () => {
vi.mocked(checkWhatsAppCredential).mockResolvedValue({
status: 'failed',
reason: 'mismatch',
});
const { getByTestId } = renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() => expect(getByTestId('whatsapp-credential-recheck')).toBeVisible());
trackMock.mockClear();
await fireEvent.click(getByTestId('whatsapp-credential-recheck'));
await waitFor(() =>
expect(trackMock).toHaveBeenCalledWith(
EVENT,
expect.objectContaining({ trigger: 'recheck', status: 'failed', reason: 'mismatch' }),
),
);
});
describe('links to Meta', () => {
const LINK_EVENT = TELEMETRY_EVENT.AGENTS.USER_CLICKED_WHATSAPP_CHANNEL_LINK;
it('tracks the Meta App Dashboard button in the setup', async () => {
const { getByTestId } = renderComponent({ props: props() });
await flushPromises();
await fireEvent.click(getByTestId('whatsapp-meta-dashboard-link'));
expect(trackMock).toHaveBeenCalledWith(
LINK_EVENT,
expect.objectContaining({ agent_id: 'a', target: 'meta_dashboard' }),
);
});
it.each([
['whatsapp-manager-link', 'whatsapp_manager'],
['whatsapp-find-meta-app', 'meta_app'],
])('tracks %s in the settings as %s', async (testId, target) => {
const { getByTestId } = renderComponent({
props: props({ mode: 'edit', connected: true, modelValue: 'cred-1' }),
});
await waitFor(() => expect(getByTestId(testId)).toBeVisible());
await fireEvent.click(getByTestId(testId));
expect(trackMock).toHaveBeenCalledWith(
LINK_EVENT,
expect.objectContaining({ agent_id: 'a', target }),
);
});
});
it('does not track the settings, which check again on every open', async () => {
renderComponent({
props: props({ mode: 'edit', connected: true, modelValue: 'cred-1' }),
});
await flushPromises();
expect(trackMock).not.toHaveBeenCalledWith(EVENT, expect.anything());
});
it('does not send the credential or the profile', async () => {
renderComponent({ props: props({ modelValue: 'cred-1' }) });
await waitFor(() => expect(trackMock).toHaveBeenCalledWith(EVENT, expect.anything()));
const payload = JSON.stringify(trackMock.mock.calls);
expect(payload).not.toContain('cred-1');
expect(payload).not.toContain(PROFILE.verifiedName);
expect(payload).not.toContain(PROFILE.displayPhoneNumber);
});
});
describe('connect', () => {
const NEEDS = 'agents.channels.whatsapp.setup.connect';

View File

@ -19,6 +19,7 @@ import {
} from '@/features/credentials/credentials.store';
import { TIME } from '@/app/constants';
import { getWhatsAppVerifyToken } from '../../composables/useAgentApi';
import { useAgentTelemetry } from '../../composables/useAgentTelemetry';
import AgentIntegrationCredentialConnection from '../../components/AgentIntegrationCredentialConnection.vue';
import type { AgentCredentialOption } from '../../components/AgentCredentialSelect.vue';
import AgentChannelWhatsAppIdentityCard from './AgentChannelWhatsAppIdentityCard.vue';
@ -76,6 +77,7 @@ const emit = defineEmits<{
const i18n = useI18n();
const rootStore = useRootStore();
const toast = useToast();
const agentTelemetry = useAgentTelemetry();
const META_APP_DASHBOARD_URL = 'https://developers.facebook.com/apps';
@ -142,7 +144,35 @@ const profile = computed(() =>
credentialCheck.value?.status === 'ok' ? credentialCheck.value.profile : null,
);
async function runCredentialCheck() {
type CredentialCheckFailure = Extract<CredentialCheckResult, { status: 'failed' }>['reason'];
/** The names the warehouse knows the reasons by, apart from how the view words them. */
const CHECK_FAILURE_REASON = {
incomplete: 'incomplete',
temporaryToken: 'temporary_token',
mismatch: 'mismatch',
unreachable: 'unreachable',
notFound: 'not_found',
} as const satisfies Record<CredentialCheckFailure, string>;
/** Only the setup is measured: the settings check the credential again on every open. */
function trackCredentialCheck(
trigger: 'auto' | 'recheck',
result: CredentialCheckResult,
requestFailed: boolean,
) {
if (props.mode !== 'setup') return;
agentTelemetry.trackCheckedWhatsAppCredential({
agentId: props.agentId,
trigger,
status: result.status,
...(result.status === 'failed'
? { reason: requestFailed ? 'request_failed' : CHECK_FAILURE_REASON[result.reason] }
: {}),
});
}
async function runCredentialCheck(trigger: 'auto' | 'recheck' = 'auto') {
const request = ++latestCheck;
const id = credentialId.value;
// A result for the credential just replaced says nothing about this one.
@ -159,6 +189,7 @@ async function runCredentialCheck() {
if (request !== latestCheck || unmounted) return;
checking.value = false;
credentialCheck.value = { status: 'failed', reason: 'unreachable' };
trackCredentialCheck(trigger, credentialCheck.value, true);
toast.showError(error, i18n.baseText('agents.channels.modal.saveChannelError'));
return;
}
@ -168,6 +199,9 @@ async function runCredentialCheck() {
}
let result: CredentialCheckResult;
// Shown as unreachable, but kept apart in telemetry: a failed request to n8n says
// nothing about whether Meta accepts the credential.
let requestFailed = false;
try {
result = await checkWhatsAppCredential(
rootStore.restApiContext,
@ -178,13 +212,15 @@ async function runCredentialCheck() {
} catch (error) {
const notFound = error instanceof ResponseError && error.httpStatusCode === 404;
result = { status: 'failed', reason: notFound ? 'notFound' : 'unreachable' };
requestFailed = !notFound;
}
if (request !== latestCheck) return;
credentialCheck.value = result;
checking.value = false;
trackCredentialCheck(trigger, result, requestFailed);
}
watch(credentialId, runCredentialCheck, { immediate: true });
watch(credentialId, () => runCredentialCheck(), { immediate: true });
// Saving the picked credential keeps its ID, so the watch above does not see the edit.
// Pinia removes this listener when the component unmounts.
@ -427,6 +463,9 @@ defineExpose({ credentialId, currentSettings, validationError: null, afterSave }
size="medium"
icon="meta"
data-testid="whatsapp-meta-dashboard-link"
@click="
agentTelemetry.trackClickedWhatsAppLink({ agentId, target: 'meta_dashboard' })
"
>
{{ i18n.baseText('agents.channels.whatsapp.setup.createApp.button') }}
</N8nButton>
@ -479,7 +518,7 @@ defineExpose({ credentialId, currentSettings, validationError: null, afterSave }
variant="ghost"
size="small"
data-testid="whatsapp-credential-recheck"
@click="runCredentialCheck"
@click="runCredentialCheck('recheck')"
>
{{ i18n.baseText('agents.channels.whatsapp.setup.credential.recheck') }}
</N8nButton>
@ -619,6 +658,9 @@ defineExpose({ credentialId, currentSettings, validationError: null, afterSave }
:personalisation="personalisation"
:ready="credentialVerified"
show-manager-link
@open-manager="
agentTelemetry.trackClickedWhatsAppLink({ agentId, target: 'whatsapp_manager' })
"
/>
<N8nText
v-if="credentialProblem"
@ -638,6 +680,7 @@ defineExpose({ credentialId, currentSettings, validationError: null, afterSave }
rel="noopener noreferrer"
size="small"
data-testid="whatsapp-find-meta-app"
@click="agentTelemetry.trackClickedWhatsAppLink({ agentId, target: 'meta_app' })"
>
<span :class="$style.linkContent">
{{ i18n.baseText('agents.channels.whatsapp.settings.findMetaApp') }}

View File

@ -174,6 +174,49 @@ export function useAgentTelemetry() {
});
}
function trackCheckedWhatsAppCredential(params: {
agentId: string;
trigger: 'auto' | 'recheck';
status: 'ok' | 'failed';
reason?: InferTelemetryProps<
typeof TELEMETRY_EVENT.AGENTS.USER_CHECKED_WHATSAPP_CHANNEL_CREDENTIAL
>['reason'];
}) {
safeTrack(TELEMETRY_EVENT.AGENTS.USER_CHECKED_WHATSAPP_CHANNEL_CREDENTIAL, {
agent_id: params.agentId,
trigger: params.trigger,
status: params.status,
...(params.reason ? { reason: params.reason } : {}),
...common(),
});
}
function trackClickedWhatsAppLink(params: {
agentId: string;
target: InferTelemetryProps<
typeof TELEMETRY_EVENT.AGENTS.USER_CLICKED_WHATSAPP_CHANNEL_LINK
>['target'];
}) {
safeTrack(TELEMETRY_EVENT.AGENTS.USER_CLICKED_WHATSAPP_CHANNEL_LINK, {
agent_id: params.agentId,
target: params.target,
...common(),
});
}
function trackSawWhatsAppReviewState(params: {
agentId: string;
state: InferTelemetryProps<
typeof TELEMETRY_EVENT.AGENTS.USER_SAW_WHATSAPP_REVIEW_STATE
>['state'];
}) {
safeTrack(TELEMETRY_EVENT.AGENTS.USER_SAW_WHATSAPP_REVIEW_STATE, {
agent_id: params.agentId,
state: params.state,
...common(),
});
}
function trackClickedDeployToAzure(params: { agentId: string }) {
safeTrack(TELEMETRY_EVENT.AGENTS.USER_CLICKED_DEPLOY_TO_AZURE_FOR_TEAMS_CHANNEL, {
agent_id: params.agentId,
@ -268,6 +311,9 @@ export function useAgentTelemetry() {
trackClosedChannelSetup,
trackFailedToConnectChannel,
trackCheckedTeamsCredential,
trackCheckedWhatsAppCredential,
trackClickedWhatsAppLink,
trackSawWhatsAppReviewState,
trackClickedDeployToAzure,
trackDownloadedTeamsAppPackage,
trackSelectedN8nChatAgent,