Path: blob/main/extensions/copilot/src/extension/test/node/telemetry.spec.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { suite, test } from 'vitest';7import { ChatLocation } from '../../../platform/chat/common/commonTypes';8import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';9import { createPlatformServices } from '../../../platform/test/node/services';10import { allEvents, withTelemetryCapture } from '../../../platform/test/node/telemetry';11import { createTelemetryWithId, getCodeBlocks, sendConversationalMessageTelemetry, sendUserActionTelemetry } from '../../prompt/node/telemetry';1213// TODO @lramos15 Re-enable once telemetry has been fully cleaned up14suite.skip('Conversation telemetry tests', { timeout: 10000 }, function () {1516test('Test telemetryMessage', async function () {17// Set up inputs18const testingServiceCollection = createPlatformServices();19const document = undefined;20const messageText = 'hello world!';21const messageLen = 12;22const prompt = 'You are a programming assistant. Respond to the question hello world!';23const source = 'user';24const turnIndex = 0;25const intentClassifierScore = 1;26const intentClassifierLatency = 10;2728// Call function29const [messages] = await withTelemetryCapture(testingServiceCollection, async accessor => {30const telemetryData = sendConversationalMessageTelemetry(31accessor.get(ITelemetryService),32document,33ChatLocation.Panel,34messageText,35{ source: source, turnIndex: turnIndex.toString() },36{37messageCharLen: messageLen,38promptCharLen: prompt.length,39intentClassifierScore: intentClassifierScore,40intentClassifierLatency: intentClassifierLatency,41},42createTelemetryWithId()43);4445// Check that properties and measurements for standard telemetry are correct46assert.strictEqual(telemetryData.properties.source, source);47assert.strictEqual(telemetryData.properties.turnIndex, turnIndex.toString());48assert.strictEqual(telemetryData.measurements.messageCharLen, messageText.length);49assert.strictEqual(telemetryData.measurements.promptCharLen, prompt.length);50assert.strictEqual(telemetryData.measurements.intentClassifierScore, intentClassifierScore);51assert.strictEqual(telemetryData.measurements.intentClassifierLatency, intentClassifierLatency);5253// Check that enhanced telemetry fields are not in standard telemetry data54assert(!('messageText' in telemetryData.properties));55});5657// All of the below adapted from the ghostText telemetry integration tests58assert.ok(allEvents(messages));59const names = messages60.map(message => message.data.baseData.name.split('/')[1])61// In case we need a new Copilot token, we don't care about the messages that triggers62.filter(name => !['auth.new_login', 'auth.new_token'].includes(name));63// Correct events are created64assert.deepStrictEqual(65names.filter(name => !name.startsWith('engine.') && name !== 'log').sort(),66['conversation.message', 'conversation.messageText'].sort()67);68// Correct properties are attached to the message events69assert.ok(70messages71.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')72.every(message => message.data.baseData.properties.source === source)73);74assert.ok(75messages76.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')77.every(message => message.data.baseData.properties.turnIndex === turnIndex.toString())78);79// Correct measurements are attached to the message events80assert.ok(81messages82.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')83.every(message => message.data.baseData.measurements.messageCharLen === messageLen)84);85assert.ok(86messages87.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')88.every(message => message.data.baseData.measurements.promptCharLen === prompt.length)89);90assert.ok(91messages92.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')93.every(message => message.data.baseData.measurements.intentClassifierScore === intentClassifierScore)94);95assert.ok(96messages97.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.message')98.every(message => message.data.baseData.measurements.intentClassifierLatency === intentClassifierLatency)99);100// Correct properties are attached to the messageText events101assert.ok(102messages103.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.messageText')104.every(message => message.data.baseData.properties.messageText === messageText)105);106});107108test('Test telemetryUserAction', async function () {109// Set up inputs110const testingServiceCollection = createPlatformServices();111const document = undefined;112const rating = 'positive';113const messageId = '12345';114const name = 'conversation.messageRating';115116// Call function117const [messages] = await withTelemetryCapture(testingServiceCollection, async accessor => {118const telemetryData = sendUserActionTelemetry(accessor.get(ITelemetryService), document, { rating: rating, messageId: messageId }, {}, name);119120// Check that properties and measurements for standard telemetry are correct121assert.strictEqual(telemetryData.properties.rating, rating);122assert.strictEqual(telemetryData.properties.messageId, messageId);123});124125// All of the below adapted from the ghostText telemetry integration tests126assert.ok(allEvents(messages));127const names = messages128.map(message => message.data.baseData.name.split('/')[1])129// In case we need a new Copilot token, we don't care about the messages that triggers130.filter(name => !['auth.new_login', 'auth.new_token'].includes(name));131// Correct events are created132assert.deepStrictEqual(133names.filter(name => !name.startsWith('engine.') && name !== 'log').sort(),134['conversation.messageRating'].sort()135);136// Correct properties are attached to the message events137assert.ok(138messages139.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.messageRating')140.every(message => message.data.baseData.properties.rating === rating)141);142assert.ok(143messages144.filter(message => message.data.baseData.name.split('/')[1] === 'conversation.messageRating')145.every(message => message.data.baseData.properties.messageId === messageId)146);147});148149test('Test getCodeBlocks with no code', async function () {150// Set up inputs151const noCode = 'hello world';152153// Test no code case154const noCodeResult = getCodeBlocks(noCode);155assert.strictEqual(noCodeResult.length, 0, 'Length of no code result should be 0');156});157158test('Test getCodeBlocks with one code block, no language', async function () {159// Set up inputs160const basicNoLang = '\n```\nhello world\n```';161162// Test basic no lang case163const basicNoLangResult = getCodeBlocks(basicNoLang);164assert.deepEqual(basicNoLangResult, [''], 'Basic no lang result should be an array with an empty string');165});166167test('Test getCodeBlocks with one code block with language', async function () {168// Set up inputs169const basicWithLang = '\n```python\nhello world\n```';170171// Test basic with lang case172const basicWithLangResult = getCodeBlocks(basicWithLang);173assert.deepEqual(174basicWithLangResult,175['python'],176'Basic with lang result should be an array with a single string'177);178});179180test('Test getCodeBlocks with nested code blocks', async function () {181// Set up inputs182const nested = '\n```\n```python\ndef hello_world():\n print("Hello, world!")\n```\n```\n\n';183184// Test nested case185const nestedResult = getCodeBlocks(nested);186assert.deepEqual(187nestedResult,188[''],189'Nested result should be an array with one empty string, ignoring backticks within the code block'190);191});192193test('Test getCodeBlocks with multiple nested code blocks', async function () {194// Set up inputs195const multiNested =196'\n```\n```python\ndef hello_world():\n print(\'Hello, world!\'\')\n```\n```\n\nThis will render as:\n\n```python\ndef hello_world():\n print(\'Hello, world!\'\')\n```';197198// Test multi nested case199const multiNestedResult = getCodeBlocks(multiNested);200assert.deepEqual(201multiNestedResult,202['', ''],203'Multi nested result should be an array with two empty strings, ignoring backticks within the code block'204);205});206207test('Test getCodeBlocks with escaped backticks', async function () {208// Set up inputs209const escaped =210'\n\n\\`\\`\\`python\nprint("Hello, world!")\n\\`\\`\\`\n\nThis will produce:\n\n```python\nprint("Hello, world!")\n```';211212// Test escaped case213const escapedResult = getCodeBlocks(escaped);214assert.deepEqual(215escapedResult,216['python'],217'Escaped result should be an array with a single string, ignoring escaped backticks'218);219});220});221222223