diff --git a/shared/badges.ts b/shared/badges.ts index 6adf3f31f..0a08d51ef 100644 --- a/shared/badges.ts +++ b/shared/badges.ts @@ -1,4 +1,5 @@ import { Base64 } from 'js-base64' +import * as sourcegraph from 'sourcegraph' /** * Creates a base64-encoded image URI. @@ -60,7 +61,7 @@ function makeInfoIcon(color: string): string { /** * The badge to send back on all results that come from searched-based data. */ -export const impreciseBadge = { +export const impreciseBadge: sourcegraph.BadgeAttachmentRenderOptions = { icon: makeInfoIcon('#ffffff'), light: { icon: makeInfoIcon('#000000') }, hoverMessage: diff --git a/shared/providers.ts b/shared/providers.ts index 993e79b3b..10d98ee70 100644 --- a/shared/providers.ts +++ b/shared/providers.ts @@ -6,6 +6,7 @@ import { LanguageSpec } from './language-specs/spec' import { Logger } from './logging' import { createProviders as createLSIFProviders } from './lsif/providers' import { createProviders as createSearchProviders } from './search/providers' +import { TelemetryEmitter } from './telemetry' import { asArray, mapArrayish } from './util/helpers' import { noopAsyncGenerator, observableFromAsyncIterator } from './util/ix' @@ -145,9 +146,12 @@ export function createDefinitionProvider( doc: sourcegraph.TextDocument, pos: sourcegraph.Position ): AsyncGenerator { + const emitter = new TelemetryEmitter() + let lastLsifResult: sourcegraph.Definition | undefined for await (const lsifResult of lsifProvider(doc, pos)) { if (lsifResult) { + await emitter.emitOnce('lsifDefinitions') yield lsifResult lastLsifResult = lsifResult } @@ -158,19 +162,24 @@ export function createDefinitionProvider( } if (lspProvider) { - // Delegate to LSP if it's available. Do not try to supplement + for await (const lspResult of lspProvider(doc, pos)) { + await emitter.emitOnce('lspDefinitions') + yield lspResult + } + + // Do not try to supplement // with additional search results as we have all the context we // need for complete and precise results here. - yield* lspProvider(doc, pos) return } for await (const searchResult of searchProvider(doc, pos)) { - // No results so far, fall back to search. Mark result as imprecise. - yield mapArrayish(searchResult, location => ({ - ...location, - badge: impreciseBadge, - })) + // No results so far, fall back to search. Mark the result as + // imprecise. + if (searchResult) { + await emitter.emitOnce('searchDefinitions') + yield badgeValues(searchResult, impreciseBadge) + } } }), } @@ -199,9 +208,12 @@ export function createReferencesProvider( pos: sourcegraph.Position, ctx: sourcegraph.ReferenceContext ): AsyncGenerator { + const emitter = new TelemetryEmitter() + let lsifResults: sourcegraph.Location[] = [] for await (const lsifResult of lsifProvider(doc, pos, ctx)) { if (lsifResult) { + await emitter.emitOnce('lsifReferences') yield lsifResult lsifResults = lsifResult } @@ -217,6 +229,7 @@ export function createReferencesProvider( // Re-emit the last results from the previous provider // so we do not overwrite what was emitted previously. + await emitter.emitOnce('lspReferences') yield lsifResults.concat(filteredResults) } @@ -243,11 +256,9 @@ export function createReferencesProvider( // Re-emit the last results from the previous provider so we // do not overwrite what was emitted previously. Mark new results // as imprecise. + await emitter.emitOnce('searchReferences') yield lsifResults.concat( - filteredResults.map(location => ({ - ...location, - badge: impreciseBadge, - })) + asArray(badgeValues(filteredResults, impreciseBadge)) ) } }), @@ -275,9 +286,12 @@ export function createHoverProvider( void, undefined > { + const emitter = new TelemetryEmitter() + let lastLsifResult: sourcegraph.Hover | null | undefined for await (const lsifResult of lsifProvider(doc, pos)) { if (lsifResult) { + await emitter.emitOnce('lsifHover') yield lsifResult lastLsifResult = lsifResult } @@ -288,16 +302,25 @@ export function createHoverProvider( } if (lspProvider) { - // Delegate to LSP if it's available. Do not try to supplement - // with additional search results as we have all the context we - // need for complete and precise results here. - yield* lspProvider(doc, pos) + // Delegate to LSP if it's available. + for await (const lspResult of lspProvider(doc, pos)) { + if (lspResult) { + await emitter.emitOnce('lspHover') + yield lspResult + } + } + + // Do not try to supplement with additional search results + // as we have all the context we need for complete and precise + // results here. return } for await (const searchResult of searchProvider(doc, pos)) { - // No results so far, fall back to search. Mark result as imprecise. + // No results so far, fall back to search. Mark the result as + // imprecise. if (searchResult) { + await emitter.emitOnce('searchHover') yield { ...searchResult, badge: impreciseBadge } } } @@ -305,6 +328,20 @@ export function createHoverProvider( } } +/** + * Add a badge property to a single value or to a list of values. Returns the + * modified result in the same shape as the input. + * + * @param value The list of values, a single value, or null. + * @param badge The badge attachment. + */ +export function badgeValues( + value: T | T[] | null, + badge: sourcegraph.BadgeAttachmentRenderOptions +): sourcegraph.Badged | sourcegraph.Badged[] | null { + return mapArrayish(value, v => ({ ...v, badge })) +} + /** * Converts an async generator provider into an observable provider. This also * memoizes the previous result as a workaround for #1321 (below). diff --git a/shared/telemetry.ts b/shared/telemetry.ts new file mode 100644 index 000000000..143466350 --- /dev/null +++ b/shared/telemetry.ts @@ -0,0 +1,49 @@ +import * as sourcegraph from 'sourcegraph' + +/** + * A wrapper around telemetry events. A new instance of this class + * should be instantiated at the start of each action as it handles + * latency tracking. + */ +export class TelemetryEmitter { + private started: number + private emitted = new Set() + + constructor() { + this.started = Date.now() + } + + /** + * Emit a telemetry event with a durationMs attribute only if the + * same action has not yet emitted for this instance. + */ + public emitOnce(action: string, args: object = {}): Promise { + if (this.emitted.has(action)) { + return Promise.resolve() + } + + this.emitted.add(action) + return this.emit(action, args) + } + + /** + * Emit a telemetry event with a durationMs attribute. + */ + public async emit(action: string, args: object = {}): Promise { + try { + await sourcegraph.commands.executeCommand( + 'logTelemetryEvent', + `codeintel.${action}`, + { ...args, durationMs: this.elapsed() } + ) + } catch { + // Older version of Sourcegraph may have not registered this + // command, causing the promise to reject. We can safely ignore + // this condition. + } + } + + private elapsed(): number { + return Date.now() - this.started + } +} diff --git a/shared/util/helpers.ts b/shared/util/helpers.ts index 14361f936..632d7d69e 100644 --- a/shared/util/helpers.ts +++ b/shared/util/helpers.ts @@ -17,7 +17,7 @@ export function asArray(value: T | T[] | null): T[] { } /** - * Apply a map function on a singel value or over a list of values. Returns the + * Apply a map function on a single value or over a list of values. Returns the * modified result in the same shape as the input. * * @param value The list of values, a single value, or null.