Verified Commit 9dbb76af authored by Olena Horal-Koretska's avatar Olena Horal-Koretska 2️⃣ Committed by GitLab
Browse files

fix: remove Snowplow LS startup tracking in favour of OpenTelemetry

parent 99e69b59
Loading
Loading
Loading
Loading
+0 −8
Original line number Diff line number Diff line
@@ -46,13 +46,6 @@ import { LanguageServerStartupMonitor } from './language_server_startup_monitor'
jest.mock('../code_suggestions/gitlab_platform_manager_for_code_suggestions');
jest.mock('../log'); // disable logging in tests
jest.mock('../feature_flags/local_feature_flag_service');
jest.mock('../snowplow/snowplow', () => ({
  Snowplow: {
    getInstance: jest
      .fn()
      .mockReturnValue({ trackStructEvent: jest.fn().mockResolvedValue(undefined) }),
  },
}));

jest.mock('../webview');

@@ -123,7 +116,6 @@ describe('LanguageClientWrapper', () => {
  const createWrapperWithMonitor = (options: Parameters<typeof createWrapper>[0] = {}) => {
    const wrapperWithMonitor = createWrapper(options);
    wrapperWithMonitor.startupMonitor = new LanguageServerStartupMonitor(
      options.mockTelemetryEnvironment ?? gitLabTelemetryEnvironment,
      new NoopObservabilityService(),
    );
    return wrapperWithMonitor;
+1 −4
Original line number Diff line number Diff line
@@ -112,10 +112,7 @@ export class LanguageServerManager implements WebviewManager, VersionProvider {
    );
    const gutterIcon = new CodeSuggestionsGutterIcon(this.#context, stateManager);
    const middleware = new LanguageClientMiddleware(stateManager);
    const startupMonitor = new LanguageServerStartupMonitor(
      gitLabTelemetryEnvironment,
      observabilityService,
    );
    const startupMonitor = new LanguageServerStartupMonitor(observabilityService);
    const statusBarItem = new DuoStatusBarItem(stateManager, startupMonitor);
    const startupStatusBarItem = new LanguageServerStartupStatusBarItem(startupMonitor);
    const baseAssetsUrl = vscode.Uri.joinPath(
+1 −87
Original line number Diff line number Diff line
import { BaseLanguageClient, State, StateChangeEvent } from 'vscode-languageclient';
import { createFakePartial } from '../test_utils/create_fake_partial';
import { log } from '../log';
import { Snowplow } from '../snowplow/snowplow';
import { GitLabTelemetryEnvironment } from '../platform/gitlab_telemetry_environment';
import { NoopObservabilityService } from '../observability';
import { LanguageServerStartupMonitor } from './language_server_startup_monitor';

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

describe('LanguageServerStartupMonitor', () => {
  let monitor: LanguageServerStartupMonitor;
  let onDidChangeStateHandler: (event: StateChangeEvent) => void;
  let client: BaseLanguageClient;
  let trackStructEvent: jest.Mock;

  beforeEach(() => {
    jest.useFakeTimers();
    jest.clearAllMocks();
    trackStructEvent = jest.fn().mockResolvedValue(undefined);
    jest
      .mocked(Snowplow.getInstance)
      .mockReturnValue(createFakePartial<Snowplow>({ trackStructEvent }));
    const telemetryEnvironment = createFakePartial<GitLabTelemetryEnvironment>({
      getOs: jest.fn().mockReturnValue('linux'),
    });
    monitor = new LanguageServerStartupMonitor(
      telemetryEnvironment,
      new NoopObservabilityService(),
    );
    monitor = new LanguageServerStartupMonitor(new NoopObservabilityService());
    client = createFakePartial<BaseLanguageClient>({
      onDidChangeState: jest.fn(handler => {
        onDidChangeStateHandler = handler;
@@ -144,28 +128,6 @@ describe('LanguageServerStartupMonitor', () => {

      await expect(promise).resolves.toBeUndefined();
      expect(monitor.phase).toBe('running');

      jest.advanceTimersByTime(10000);
      expect(trackStructEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          category: 'language_server',
          action: 'startup_completed',
          property: 'success',
        }),
        expect.arrayContaining([
          expect.objectContaining({
            data: expect.objectContaining({
              extra: expect.objectContaining({
                status: 'success',
                failed_phase: null,
                os: 'linux',
                remote_name: null,
              }),
            }),
          }),
          'ide-extension-context',
        ]),
      );
    });

    it('does not reject when State.Stopped fires - lets the library handle restarts', async () => {
@@ -186,22 +148,6 @@ describe('LanguageServerStartupMonitor', () => {

        await expect(promise).rejects.toThrow('Language Server process did not start within 10s');
        expect(monitor.phase).toBe('failed');
        jest.advanceTimersByTime(10000);
        expect(trackStructEvent).toHaveBeenCalledWith(
          expect.objectContaining({ property: 'failed' }),
          expect.arrayContaining([
            expect.objectContaining({
              data: expect.objectContaining({
                extra: expect.objectContaining({
                  status: 'failed',
                  failed_phase: 'idle',
                  os: 'linux',
                  remote_name: null,
                }),
              }),
            }),
          ]),
        );
      });

      it('clears spawn timeout when State.Starting fires', async () => {
@@ -224,22 +170,6 @@ describe('LanguageServerStartupMonitor', () => {
          'Language Server process started but LSP initialize handshake did not complete within 30s',
        );
        expect(monitor.phase).toBe('failed');
        jest.advanceTimersByTime(10000);
        expect(trackStructEvent).toHaveBeenCalledWith(
          expect.objectContaining({ property: 'failed' }),
          expect.arrayContaining([
            expect.objectContaining({
              data: expect.objectContaining({
                extra: expect.objectContaining({
                  status: 'failed',
                  failed_phase: 'spawning',
                  os: 'linux',
                  remote_name: null,
                }),
              }),
            }),
          ]),
        );
      });

      it('clears handshake timeout when State.Running fires', async () => {
@@ -288,22 +218,6 @@ describe('LanguageServerStartupMonitor', () => {
        expect(log.debug).toHaveBeenCalledWith(
          expect.stringContaining('failed to restart after 1 attempt(s)'),
        );
        jest.advanceTimersByTime(10000);
        expect(trackStructEvent).toHaveBeenCalledWith(
          expect.objectContaining({ property: 'failed' }),
          expect.arrayContaining([
            expect.objectContaining({
              data: expect.objectContaining({
                extra: expect.objectContaining({
                  status: 'failed',
                  failed_phase: 'spawning',
                  os: 'linux',
                  remote_name: null,
                }),
              }),
            }),
          ]),
        );
      });

      it('does not reject when State.Starting follows State.Stopped within grace period', async () => {
+6 −60
Original line number Diff line number Diff line
@@ -2,10 +2,6 @@ import { BaseLanguageClient, Disposable, State } from 'vscode-languageclient';
import * as vscode from 'vscode';
import type { Histogram, Span, Tracer } from '@opentelemetry/api';
import { log } from '../log';
import { Snowplow } from '../snowplow/snowplow';
import { EXTENSION_EVENT_SOURCE, GITLAB_STANDARD_SCHEMA_URL } from '../snowplow/snowplow_options';
import { GitLabEnvironment } from '../snowplow/get_environment';
import { GitLabTelemetryEnvironment } from '../platform/gitlab_telemetry_environment';
import type { ObservabilityService } from '../observability';

export type StartupPhase = 'idle' | 'spawning' | 'handshake' | 'running' | 'failed';
@@ -15,8 +11,6 @@ const HANDSHAKE_TIMEOUT_MS = 30000;
const RESTART_GRACE_PERIOD_MS = 5000;

export class LanguageServerStartupMonitor {
  #telemetryEnvironment: GitLabTelemetryEnvironment;

  #phase: StartupPhase = 'idle';

  #tProcessStarting = 0;
@@ -43,11 +37,7 @@ export class LanguageServerStartupMonitor {

  #histogram: Histogram | null;

  constructor(
    telemetryEnvironment: GitLabTelemetryEnvironment,
    observabilityService: ObservabilityService,
  ) {
    this.#telemetryEnvironment = telemetryEnvironment;
  constructor(observabilityService: ObservabilityService) {
    this.#tracer = observabilityService.getTracer('gitlab-vscode-extension.language-server');
    this.#histogram =
      observabilityService
@@ -83,55 +73,19 @@ export class LanguageServerStartupMonitor {
    this.#setPhase('handshake');
  }

  #trackStartupTelemetry({
  #recordStartupMetrics({
    status,
    failedPhase,
    spawnMs,
    handshakeMs,
    totalMs,
  }: {
    status: 'success' | 'failed';
    failedPhase: StartupPhase | null;
    spawnMs: number;
    handshakeMs: number;
    totalMs: number;
  }): void {
    const context = {
      schema: GITLAB_STANDARD_SCHEMA_URL,
      data: {
        source: EXTENSION_EVENT_SOURCE,
        environment: GitLabEnvironment.GITLAB_SELF_MANAGED,
        extra: {
          status,
          failed_phase: failedPhase,
          spawn_duration_ms: Math.round(spawnMs),
          handshake_duration_ms: Math.round(handshakeMs),
          total_duration_ms: Math.round(totalMs),
          restart_attempts: this.#restartAttempts,
          os: this.#telemetryEnvironment.getOs(),
          remote_name: vscode.env.remoteName ?? null,
        },
      },
    };

    this.#histogram?.record(Math.round(totalMs), {
      status,
      failed_phase: failedPhase ?? '',
    });

    setTimeout(() => {
      Snowplow.getInstance()
        .trackStructEvent(
          {
            category: 'language_server',
            action: 'startup_completed',
            label: 'status',
            property: status,
          },
          [context, 'ide-extension-context'],
        )
        .catch(err => log.warn('[LS startup] Failed to track startup telemetry', err));
    }, 10000);
  }

  observe(client: BaseLanguageClient): Promise<void> {
@@ -156,11 +110,9 @@ export class LanguageServerStartupMonitor {
        });
        this.#startupSpan?.end();
        this.#startupSpan = undefined;
        this.#trackStartupTelemetry({
        this.#recordStartupMetrics({
          status: 'failed',
          failedPhase: 'idle',
          spawnMs: 0,
          handshakeMs: 0,
          totalMs: SPAWN_TIMEOUT_MS,
        });
        fail(
@@ -198,11 +150,9 @@ export class LanguageServerStartupMonitor {
              this.#startupSpan?.end();
              this.#startupSpan = undefined;
              this.#setPhase('failed');
              this.#trackStartupTelemetry({
              this.#recordStartupMetrics({
                status: 'failed',
                failedPhase,
                spawnMs,
                handshakeMs: HANDSHAKE_TIMEOUT_MS,
                totalMs: spawnMs + HANDSHAKE_TIMEOUT_MS,
              });
              fail(
@@ -232,11 +182,9 @@ export class LanguageServerStartupMonitor {
            );
            clearTimeout(this.#handshakeTimeout);
            this.#handshakeTimeout = undefined;
            this.#trackStartupTelemetry({
            this.#recordStartupMetrics({
              status: 'success',
              failedPhase: null,
              spawnMs,
              handshakeMs,
              totalMs,
            });
            resolve();
@@ -262,11 +210,9 @@ export class LanguageServerStartupMonitor {
              this.#startupSpan?.end();
              this.#startupSpan = undefined;
              this.#setPhase('failed');
              this.#trackStartupTelemetry({
              this.#recordStartupMetrics({
                status: 'failed',
                failedPhase: 'spawning',
                spawnMs: 0,
                handshakeMs: 0,
                totalMs: RESTART_GRACE_PERIOD_MS,
              });
              log.debug(