Commit 7c027f48 authored by Nicolas Dular's avatar Nicolas Dular
Browse files

feat: Send feedback for gitlab duo chat

This connects the callback from the `duo-chat` component to our snowplow
tracking where we gather the feedback for responses from GitLab duo
chat.
parent 6057ef81
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import { GitLabChatController } from './gitlab_chat_controller';
import { GitLabPlatformManager } from '../platform/gitlab_platform';
import { GitLabChatRecord } from './gitlab_chat_record';
import { GitLabChatFileContext } from './gitlab_chat_file_context';
import { submitFeedback } from './utils/submit_feedback';

const apiMock = { processNewUserPrompt: jest.fn(), pullAiMessage: jest.fn() };

@@ -10,6 +11,10 @@ jest.mock('./gitlab_chat_api', () => ({
  GitLabChatApi: jest.fn().mockImplementation(() => apiMock),
}));

jest.mock('./utils/submit_feedback', () => ({
  submitFeedback: jest.fn(),
}));

const viewMock = {
  addRecord: jest.fn(),
  updateRecord: jest.fn(),
@@ -224,4 +229,47 @@ describe('GitLabChatController', () => {
      });
    });
  });

  describe('viewMessageHandler', () => {
    describe('supports trackFeedback events', () => {
      it('calls submitFeedback when data is present', async () => {
        await controller.viewMessageHandler({
          eventType: 'trackFeedback',
          data: {
            extendedTextFeedback: 'free text',
            feedbackChoices: ['choice1', 'choice2'],
          },
        });

        expect(submitFeedback).toHaveBeenCalledWith('free text', ['choice1', 'choice2']);
      });

      it('does not call submitFeedback when no data is present', async () => {
        await controller.viewMessageHandler({
          eventType: 'trackFeedback',
        });

        expect(submitFeedback).not.toHaveBeenCalled();
      });
    });

    describe('supports newPrompt events', () => {
      it('processes new record', async () => {
        controller.processNewUserRecord = jest.fn();

        await controller.viewMessageHandler({
          eventType: 'newPrompt',
          record: {
            content: 'hello',
          },
        });

        expect(controller.processNewUserRecord).toHaveBeenCalledWith(
          expect.objectContaining({
            content: 'hello',
          }),
        );
      });
    });
  });
});
+11 −3
Original line number Diff line number Diff line
import * as vscode from 'vscode';
import { GitLabChatRecord } from './gitlab_chat_record';
import { GitLabChatView, ViewMessage } from './gitlab_chat_view';
import { GitLabChatView, ViewEmittedMessage } from './gitlab_chat_view';
import { GitLabChatApi } from './gitlab_chat_api';
import { GitLabPlatformManager } from '../platform/gitlab_platform';
import { log } from '../log';
import { GitLabChatFileContext } from './gitlab_chat_file_context';
import { submitFeedback } from './utils/submit_feedback';

export class GitLabChatController implements vscode.WebviewViewProvider {
  readonly chatHistory: GitLabChatRecord[];
@@ -26,7 +27,7 @@ export class GitLabChatController implements vscode.WebviewViewProvider {
    await this.restoreHistory();
  }

  async viewMessageHandler(message: ViewMessage) {
  async viewMessageHandler(message: ViewEmittedMessage) {
    switch (message.eventType) {
      case 'newPrompt': {
        const record = new GitLabChatRecord({ role: 'user', content: message.record.content });
@@ -34,8 +35,15 @@ export class GitLabChatController implements vscode.WebviewViewProvider {
        await this.processNewUserRecord(record);
        break;
      }
      case 'trackFeedback': {
        if (message.data) {
          await submitFeedback(message.data.extendedTextFeedback, message.data.feedbackChoices);
        }

        break;
      }
      default:
        log.warn(`Unhandled chat-webview message ${message.eventType}`);
        log.warn(`Unhandled chat-webview message`);
        break;
    }
  }
+10 −2
Original line number Diff line number Diff line
@@ -24,14 +24,22 @@ interface NewPromptMessage {
  };
}

export type ViewMessage = NewPromptMessage;
interface FeedbackMessage {
  eventType: 'trackFeedback';
  data?: {
    extendedTextFeedback: string | null;
    feedbackChoices: Array<string> | null;
  };
}

export type ViewEmittedMessage = NewPromptMessage | FeedbackMessage;

export class GitLabChatView {
  #context: vscode.ExtensionContext;

  #chatView?: vscode.WebviewView;

  #messageEmitter = new vscode.EventEmitter<ViewMessage>();
  #messageEmitter = new vscode.EventEmitter<ViewEmittedMessage>();

  onViewMessage = this.#messageEmitter.event;

+104 −0
Original line number Diff line number Diff line
import { submitFeedback } from './submit_feedback';

jest.mock('../../snowplow/snowplow', () => ({
  Snowplow: {
    getInstance: jest.fn().mockReturnValue({
      trackStructEvent: jest.fn(),
    }),
  },
}));

const { trackStructEvent } = jest.requireMock('../../snowplow/snowplow').Snowplow.getInstance();

describe('submitFeedback', () => {
  beforeEach(() => {
    jest.resetModules();
  });

  describe('with feedback', () => {
    it('sends snowplow event', async () => {
      await submitFeedback('Freetext feedback', ['helpful', 'fast']);

      const standardContext = {
        schema: 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-9',
        data: {
          extra: {
            extended_feedback: 'Freetext feedback',
            source: 'gitlab-vscode',
          },
        },
      };

      expect(trackStructEvent).toHaveBeenCalledWith(
        {
          category: 'ask_gitlab_chat',
          action: 'click_button',
          label: 'response_feedback',
          property: 'helpful,fast',
        },
        [standardContext],
      );
    });

    it('sends snowplow event when choices are null', async () => {
      await submitFeedback('Freetext feedback', null);

      const standardContext = {
        schema: 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-9',
        data: {
          extra: {
            extended_feedback: 'Freetext feedback',
            source: 'gitlab-vscode',
          },
        },
      };

      expect(trackStructEvent).toHaveBeenCalledWith(
        {
          category: 'ask_gitlab_chat',
          action: 'click_button',
          label: 'response_feedback',
        },
        [standardContext],
      );
    });

    it('sends snowplow event when free text feedback is null', async () => {
      await submitFeedback(null, ['helpful', 'fast']);

      const standardContext = {
        schema: 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-9',
        data: {
          extra: {
            extended_feedback: null,
            source: 'gitlab-vscode',
          },
        },
      };

      expect(trackStructEvent).toHaveBeenCalledWith(
        {
          category: 'ask_gitlab_chat',
          action: 'click_button',
          label: 'response_feedback',
          property: 'helpful,fast',
        },
        [standardContext],
      );
    });
  });

  describe('with empty feedback', () => {
    it('does not send a snowplow event', async () => {
      await submitFeedback('', []);

      expect(trackStructEvent).not.toHaveBeenCalled();
    });

    it('does not send a snowplow event when free text feedback and choices are null', async () => {
      await submitFeedback(null, null);

      expect(trackStructEvent).not.toHaveBeenCalled();
    });
  });
});
+33 −0
Original line number Diff line number Diff line
import { Snowplow } from '../../snowplow/snowplow';

export const submitFeedback = async (
  extendedTextFeedback: string | null,
  feedbackChoices: string[] | null,
) => {
  const hasFeedback = Boolean(extendedTextFeedback?.length) || Boolean(feedbackChoices?.length);

  if (!hasFeedback) {
    return;
  }

  const GITLAB_STANDARD_SCHEMA_URL = 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-9';
  const standardContext = {
    schema: GITLAB_STANDARD_SCHEMA_URL,
    data: {
      extra: {
        extended_feedback: extendedTextFeedback,
        source: 'gitlab-vscode',
      },
    },
  };

  await Snowplow.getInstance().trackStructEvent(
    {
      category: 'ask_gitlab_chat',
      action: 'click_button',
      label: 'response_feedback',
      property: feedbackChoices?.join(','),
    },
    [standardContext],
  );
};
Loading