v2t: add v2 telemetry to the client/shared folder (#62586)

This commit is contained in:
Dan Adler 2024-06-03 16:34:28 -07:00 committed by GitHub
parent b215eb9fb5
commit 8275054987
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 426 additions and 90 deletions

View File

@ -17,6 +17,7 @@ import { wrapRemoteObservable } from '@sourcegraph/shared/src/api/client/api/com
import type { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
import type { ExtensionCodeEditor } from '@sourcegraph/shared/src/api/extension/api/codeEditor'
import type { Controller } from '@sourcegraph/shared/src/extensions/controller'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockIntersectionObserver } from '@sourcegraph/shared/src/testing/MockIntersectionObserver'
import { integrationTestContext } from '@sourcegraph/shared/src/testing/testHelpers'
@ -82,6 +83,7 @@ const commonArguments = () =>
platformContext: createMockPlatformContext(),
sourcegraphURL: DEFAULT_SOURCEGRAPH_URL,
telemetryService: NOOP_TELEMETRY_SERVICE,
telemetryRecorder: noOpTelemetryRecorder,
render: RENDER,
userSignedIn: true,
minimalUI: false,

View File

@ -72,6 +72,7 @@ import {
} from '@sourcegraph/shared/src/hover/HoverOverlay'
import { getModeFromPath } from '@sourcegraph/shared/src/languages'
import type { PlatformContext, URLToFileContext } from '@sourcegraph/shared/src/platform/context'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { createURLWithUTM } from '@sourcegraph/shared/src/tracking/utm'
import {
@ -289,7 +290,7 @@ export interface FileInfoWithContent extends FileInfoWithRepoName {
content?: string
}
export interface CodeIntelligenceProps extends TelemetryProps {
export interface CodeIntelligenceProps extends TelemetryProps, TelemetryV2Props {
platformContext: Pick<
BrowserPlatformContext,
'urlToFile' | 'requestGraphQL' | 'settings' | 'refreshSettings' | 'sourcegraphURL' | 'clientApplication'
@ -321,8 +322,12 @@ function initCodeIntelligence({
extensionsController,
render,
telemetryService,
telemetryRecorder,
repoSyncErrors,
}: Pick<CodeIntelligenceProps, 'codeHost' | 'platformContext' | 'extensionsController' | 'telemetryService'> & {
}: Pick<
CodeIntelligenceProps,
'codeHost' | 'platformContext' | 'extensionsController' | 'telemetryService' | 'telemetryRecorder'
> & {
render: Renderer
mutations: Observable<MutationRecordLike[]>
repoSyncErrors: Observable<boolean>
@ -470,6 +475,7 @@ function initCodeIntelligence({
{...codeHost.hoverOverlayClassProps}
className={classNames(styles.hoverOverlay, codeHost.hoverOverlayClassProps?.className)}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
hoverRef={this.nextOverlayElement}
extensionsController={extensionsController}
location={H.createLocation(window.location)}
@ -711,6 +717,7 @@ export async function handleCodeHost({
extensionsController,
platformContext,
telemetryService,
telemetryRecorder,
render,
minimalUI,
hideActions,
@ -778,6 +785,7 @@ export async function handleCodeHost({
extensionsController,
platformContext,
telemetryService,
telemetryRecorder,
render,
mutations,
repoSyncErrors,
@ -930,6 +938,7 @@ export async function handleCodeHost({
fileInfoOrError={error}
sourcegraphURL={sourcegraphURL}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
platformContext={platformContext}
extensionsController={extensionsController}
buttonProps={codeViewEvent.toolbarButtonProps}
@ -1229,6 +1238,7 @@ export async function handleCodeHost({
fileInfoOrError={diffOrBlobInfo}
sourcegraphURL={sourcegraphURL}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
platformContext={platformContext}
extensionsController={extensionsController}
buttonProps={toolbarButtonProps}
@ -1392,6 +1402,7 @@ export function injectCodeIntelligenceToCodeHost(
extensionsController,
platformContext,
telemetryService,
telemetryRecorder: platformContext.telemetryRecorder,
render: renderWithThemeProvider as Renderer,
minimalUI,
hideActions,

View File

@ -10,6 +10,7 @@ import { type ActionNavItemsClassProps, ActionsNavItems } from '@sourcegraph/sha
import type { ContributionScope } from '@sourcegraph/shared/src/api/extension/api/context/context'
import type { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import type { DiffOrBlobInfo, FileInfoWithContent } from '../code-hosts/shared/codeHost'
@ -42,6 +43,7 @@ export interface CodeViewToolbarProps
extends PlatformContextProps<'settings' | 'requestGraphQL'>,
ExtensionsControllerProps,
TelemetryProps,
TelemetryV2Props,
CodeViewToolbarClassProps {
sourcegraphURL: string

View File

@ -126,6 +126,24 @@ export interface ActionContribution {
* (e.g., because the client is not graphical), then the client may hide the item from the toolbar.
*/
actionItem?: ActionItem
/**
* Properties to enable event telemetry to be recorded when an action is executed.
*/
telemetryProps: {
/**
* feature must be camelCase and '.'-delimited, e.g. 'myFeature.subFeature'.
*
* Most ActionContribution features should be prefixed with 'blob.' to indicate that they are actions
* that occur on text blobs.
*/
feature: string
// No `action` prop is provided, because action items only log telemetry when executed (and thus use an
// 'executed' action.
privateMetadata?: { [key: string]: any }
}
}
/**

View File

@ -25,7 +25,7 @@
"@sourcegraph/codeintellify": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/telemetry": "^0.16.0",
"@sourcegraph/template-parser": "workspace:*",
"@sourcegraph/wildcard": "workspace:*"
},

View File

@ -5,6 +5,7 @@ import type * as H from 'history'
import { subtypeOf } from '@sourcegraph/common'
import { BrandedStory } from '@sourcegraph/wildcard/src/stories'
import { noOpTelemetryRecorder } from '../telemetry'
import { NOOP_TELEMETRY_SERVICE } from '../telemetry/telemetryService'
import { ActionItem, type ActionItemComponentProps, type ActionItemProps } from './ActionItem'
@ -27,6 +28,7 @@ const commonProps = subtypeOf<Partial<ActionItemProps>>()({
location: LOCATION,
extensionsController: EXTENSIONS_CONTROLLER,
telemetryService: NOOP_TELEMETRY_SERVICE,
telemetryRecorder: noOpTelemetryRecorder,
iconClassName: 'icon-inline',
active: true,
})
@ -42,7 +44,7 @@ export default config
export const NoopAction: StoryFn = () => (
<ActionItem
{...commonProps}
action={{ id: 'a', command: undefined, actionItem: { label: 'Hello' } }}
action={{ id: 'a', command: undefined, actionItem: { label: 'Hello' }, telemetryProps: { feature: 'a' } }}
variant="actionItem"
/>
)
@ -52,7 +54,7 @@ NoopAction.storyName = 'Noop action'
export const CommandAction: StoryFn = () => (
<ActionItem
{...commonProps}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL }}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL, telemetryProps: { feature: 'a' } }}
telemetryService={NOOP_TELEMETRY_SERVICE}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
@ -76,6 +78,7 @@ export const LinkAction: StoryFn = () => (
command: 'open',
commandArguments: ['javascript:alert("link clicked")'],
actionItem: { label: 'Hello' },
telemetryProps: { feature: 'a' },
}}
variant="actionItem"
onDidExecute={onDidExecute}
@ -95,7 +98,7 @@ export const Executing: StoryFn = () => {
return (
<ActionItemExecuting
{...commonProps}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL }}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL, telemetryProps: { feature: 'a' } }}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
/>
@ -113,7 +116,7 @@ export const _Error: StoryFn = () => {
return (
<ActionItemWithError
{...commonProps}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL }}
action={{ id: 'a', command: 'c', title: 'Hello', iconURL: ICON_URL, telemetryProps: { feature: 'a' } }}
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
/>

View File

@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, test, vi } from 'vitest'
import { assertAriaEnabled, createBarrier } from '@sourcegraph/testing'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { noOpTelemetryRecorder } from '../telemetry'
import { NOOP_TELEMETRY_SERVICE } from '../telemetry/telemetryService'
import { ActionItem, windowLocation__testingOnly } from './ActionItem'
@ -20,8 +21,17 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
/>
@ -33,8 +43,17 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
@ -47,8 +66,16 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
/>
@ -60,8 +87,14 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'a', command: 'c', actionItem: { pressed: true, label: 'b' } }}
action={{
id: 'a',
command: 'c',
actionItem: { pressed: true, label: 'b' },
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
@ -74,8 +107,14 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'a', command: 'c', actionItem: { pressed: false, label: 'b' } }}
action={{
id: 'a',
command: 'c',
actionItem: { pressed: false, label: 'b' },
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
@ -88,8 +127,17 @@ describe('ActionItem', () => {
const component = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
title={<span>t2</span>}
location={history.location}
@ -105,8 +153,17 @@ describe('ActionItem', () => {
const { container, asFragment } = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
disabledDuringExecution={true}
location={history.location}
@ -131,8 +188,17 @@ describe('ActionItem', () => {
const { asFragment } = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
showLoadingSpinnerDuringExecution={true}
location={history.location}
@ -162,8 +228,17 @@ describe('ActionItem', () => {
const { asFragment } = render(
<ActionItem
active={true}
action={{ id: 'c', command: 'c', title: 't', description: 'd', iconURL: 'u', category: 'g' }}
action={{
id: 'c',
command: 'c',
title: 't',
description: 'd',
iconURL: 'u',
category: 'g',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
variant="actionItem"
disabledDuringExecution={true}
location={history.location}
@ -195,8 +270,15 @@ describe('ActionItem', () => {
const { asFragment } = renderWithBrandedContext(
<ActionItem
active={true}
action={{ id: 'c', command: 'open', commandArguments: ['https://example.com/bar'], title: 't' }}
action={{
id: 'c',
command: 'open',
commandArguments: ['https://example.com/bar'],
title: 't',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
/>
@ -210,8 +292,15 @@ describe('ActionItem', () => {
const { asFragment } = renderWithBrandedContext(
<ActionItem
active={true}
action={{ id: 'c', command: 'open', commandArguments: ['https://other.com/foo'], title: 't' }}
action={{
id: 'c',
command: 'open',
commandArguments: ['https://other.com/foo'],
title: 't',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
/>
@ -225,9 +314,16 @@ describe('ActionItem', () => {
const { asFragment } = renderWithBrandedContext(
<ActionItem
active={true}
action={{ id: 'c1', command: 'whatever', title: 'primary' }}
altAction={{ id: 'c2', command: 'open', commandArguments: ['https://other.com/foo'], title: 'alt' }}
action={{ id: 'c1', command: 'whatever', title: 'primary', telemetryProps: { feature: 'a' } }}
altAction={{
id: 'c2',
command: 'open',
commandArguments: ['https://other.com/foo'],
title: 'alt',
telemetryProps: { feature: 'a' },
}}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
location={history.location}
extensionsController={NOOP_EXTENSIONS_CONTROLLER}
/>

View File

@ -21,6 +21,7 @@ import {
import type { ExecuteCommandParameters } from '../api/client/mainthread-api'
import { urlForOpenPanel } from '../commands/commands'
import type { ExtensionsControllerProps } from '../extensions/controller'
import type { TelemetryV2Props } from '../telemetry'
import type { TelemetryProps } from '../telemetry/telemetryService'
import styles from './ActionItem.module.scss'
@ -59,7 +60,7 @@ export interface ActionItemComponentProps extends ExtensionsControllerProps<'exe
actionItemStyleProps?: ActionItemStyleProps
}
export interface ActionItemProps extends ActionItemAction, ActionItemComponentProps, TelemetryProps {
export interface ActionItemProps extends ActionItemAction, ActionItemComponentProps, TelemetryProps, TelemetryV2Props {
variant?: 'actionItem'
hideLabel?: boolean
@ -356,6 +357,28 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State, type
// Record action ID (but not args, which might leak sensitive data).
this.props.telemetryService.log(action.id)
if (action.telemetryProps) {
this.props.telemetryRecorder.recordEvent(
// 👷 HACK: We have no control over what gets sent over Comlink/
// web workers, so we depend on action contribution implementations
// to give type guidance to ensure that we don't accidentally share
// arbitrary, potentially sensitive string values. In this
// RPC handler, when passing the provided event to the
// TelemetryRecorder implementation, we forcibly cast all
// the inputs below (feature) into known types
// (the string 'feature') so that the recorder will accept
// it. DO NOT do this elsewhere!
action.telemetryProps.feature as 'feature',
'executed',
{
privateMetadata: { action: action.id, ...action.telemetryProps.privateMetadata },
}
)
} else {
this.props.telemetryRecorder.recordEvent('blob.action', 'executed', {
privateMetadata: { action: action.id },
})
}
const emitDidExecute = (): void => {
if (this.props.onDidExecute) {

View File

@ -7,6 +7,7 @@ import { ContributableMenu } from '@sourcegraph/client-api'
import type { FlatExtensionHostAPI } from '../api/contract'
import { pretendProxySubscribable, pretendRemote } from '../api/util'
import { noOpTelemetryRecorder } from '../telemetry'
import { NOOP_TELEMETRY_SERVICE } from '../telemetry/telemetryService'
import { extensionsController } from '../testing/searchTestHelpers'
@ -42,6 +43,9 @@ describe('ActionItem', () => {
label: 'Action A',
description: 'This is Action A',
},
telemetryProps: {
feature: 'a',
},
},
],
menus: {
@ -58,6 +62,7 @@ describe('ActionItem', () => {
}}
platformContext={NOOP_PLATFORM_CONTEXT}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
/>
)
})

View File

@ -17,6 +17,7 @@ import type { ContributionOptions } from '../api/extension/extensionHostApi'
import { getContributedActionItems } from '../contributions/contributions'
import type { RequiredExtensionsControllerProps } from '../extensions/controller'
import type { PlatformContextProps } from '../platform/context'
import type { TelemetryV2Props } from '../telemetry'
import type { TelemetryProps } from '../telemetry/telemetryService'
import { ActionItem, type ActionItemProps } from './ActionItem'
@ -55,6 +56,7 @@ export interface ActionsNavItemsProps
extends ActionsProps,
ActionNavItemsClassProps,
TelemetryProps,
TelemetryV2Props,
Pick<ActionItemProps, 'showLoadingSpinnerDuringExecution' | 'actionItemStyleProps'> {
/**
* If true, it renders a `<ul className="nav">...</ul>` around the items. If there are no items, it renders `null`.

View File

@ -21,7 +21,13 @@ export async function createExtensionHostClientConnection(
initData: Omit<InitData, 'initialSettings'>,
platformContext: Pick<
PlatformContext,
'settings' | 'updateSettings' | 'getGraphQLClient' | 'requestGraphQL' | 'telemetryService' | 'clientApplication'
| 'settings'
| 'updateSettings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'telemetryService'
| 'telemetryRecorder'
| 'clientApplication'
>
): Promise<{
subscription: Unsubscribable

View File

@ -7,6 +7,7 @@ import { getGraphQLClient as getGraphQLClientBase, type SuccessGraphQLResult } f
import { cache } from '../../backend/apolloCache'
import type { PlatformContext } from '../../platform/context'
import type { SettingsCascade } from '../../settings/settings'
import { noOpTelemetryRecorder } from '../../telemetry'
import type { FlatExtensionHostAPI } from '../contract'
import { pretendRemote } from '../util'
@ -23,13 +24,19 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'telemetryRecorder'
> = {
settings: EMPTY,
getGraphQLClient,
updateSettings: () => Promise.resolve(),
requestGraphQL,
clientApplication: 'other',
telemetryRecorder: noOpTelemetryRecorder,
}
const { api } = initMainThreadAPI(pretendRemote({}), platformContext)
@ -56,13 +63,19 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'telemetryRecorder'
> = {
settings: EMPTY,
getGraphQLClient,
updateSettings: () => Promise.resolve(),
requestGraphQL,
clientApplication: 'other',
telemetryRecorder: noOpTelemetryRecorder,
}
const { api } = initMainThreadAPI(pretendRemote({}), platformContext)
@ -82,7 +95,12 @@ describe('MainThreadAPI', () => {
}
const platformContext: Pick<
PlatformContext,
'updateSettings' | 'settings' | 'requestGraphQL' | 'getGraphQLClient' | 'clientApplication'
| 'updateSettings'
| 'settings'
| 'requestGraphQL'
| 'getGraphQLClient'
| 'clientApplication'
| 'telemetryRecorder'
> = {
settings: of({
subjects: [
@ -116,6 +134,7 @@ describe('MainThreadAPI', () => {
getGraphQLClient,
requestGraphQL: () => EMPTY,
clientApplication: 'other',
telemetryRecorder: noOpTelemetryRecorder,
}
const { api } = initMainThreadAPI(
@ -147,13 +166,19 @@ describe('MainThreadAPI', () => {
const platformContext: Pick<
PlatformContext,
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'telemetryRecorder'
> = {
getGraphQLClient,
settings: of(...values),
updateSettings: () => Promise.resolve(),
requestGraphQL: () => EMPTY,
clientApplication: 'other',
telemetryRecorder: noOpTelemetryRecorder,
}
const passedToExtensionHost: SettingsCascade<object>[] = []
@ -173,13 +198,19 @@ describe('MainThreadAPI', () => {
const values = new Subject<SettingsCascade<{ a: string }>>()
const platformContext: Pick<
PlatformContext,
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
| 'updateSettings'
| 'settings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'telemetryRecorder'
> = {
settings: values.asObservable(),
updateSettings: () => Promise.resolve(),
getGraphQLClient,
requestGraphQL: () => EMPTY,
clientApplication: 'other',
telemetryRecorder: noOpTelemetryRecorder,
}
const passedToExtensionHost: SettingsCascade<object>[] = []
const { subscription } = initMainThreadAPI(

View File

@ -53,6 +53,7 @@ export const initMainThreadAPI = (
| 'requestGraphQL'
| 'getStaticExtensions'
| 'telemetryService'
| 'telemetryRecorder'
| 'clientApplication'
>
): { api: MainThreadAPI; exposedToClient: ExposedToClient; subscription: Subscription } => {
@ -140,6 +141,7 @@ export const initMainThreadAPI = (
return proxySubscribable(of([]))
},
logEvent: (eventName, eventProperties) => platformContext.telemetryService?.log(eventName, eventProperties),
getTelemetryRecorder: () => platformContext.telemetryRecorder,
logExtensionMessage: (...data) => logger.log(...data),
}

View File

@ -16,6 +16,7 @@ import type { DocumentHighlight, ReferenceContext } from '../codeintel/legacy-ex
import type { Occurrence } from '../codeintel/scip'
import type { ConfiguredExtension } from '../extensions/extension'
import type { SettingsCascade } from '../settings/settings'
import type { TelemetryV2Props } from '../telemetry'
import type { SettingsEdit } from './client/services/settings'
import type { ExecutableExtension } from './extension/activation'
@ -175,9 +176,16 @@ export interface MainThreadAPI {
/**
* Log an event (by sending it to the server).
*
* @deprecated use getTelemetryRecorder().recordEvent instead
*/
logEvent: (eventName: string, eventProperties?: any) => void
/**
* Get a TelemetryRecorder for recording telemetry events to the server.
*/
getTelemetryRecorder: () => TelemetryV2Props['telemetryRecorder']
/**
* Log messages from extensions in the main thread. Makes it easier to debug extensions for applications
* in which extensions run in a different page from the main thread

View File

@ -66,7 +66,7 @@ const DEPRECATED_EXTENSION_IDS = new Set(['sourcegraph/code-stats-insights', 'so
export function activateExtensions(
state: Pick<ExtensionHostState, 'activeExtensions' | 'contributions' | 'haveInitialExtensionsLoaded' | 'settings'>,
mainAPI: Remote<Pick<MainThreadAPI, 'logEvent'>>,
mainAPI: Remote<Pick<MainThreadAPI, 'logEvent' | 'getTelemetryRecorder'>>,
createExtensionAPI: (extensionID: string) => typeof sourcegraph,
mainThreadAPIInitializations: Observable<boolean>,
/**
@ -168,6 +168,10 @@ export function activateExtensions(
.catch(() => {
// noop
})
const telemetryRecorder = await mainAPI.getTelemetryRecorder()
telemetryRecorder.recordEvent('blob.extension', 'activate', {
privateMetadata: { extensionID: telemetryExtensionID },
})
} catch (error) {
logger.error(
`Fail to log ExtensionActivation event for extension ${id}:`,

View File

@ -13,8 +13,8 @@ import {
describe('mergeContributions()', () => {
const FIXTURE_CONTRIBUTIONS_1: Evaluated<Contributions> = {
actions: [
{ id: '1.a', command: 'c', title: '1.A' },
{ id: '1.b', command: 'c', title: '1.B' },
{ id: '1.a', command: 'c', title: '1.A', telemetryProps: { feature: '1.a' } },
{ id: '1.b', command: 'c', title: '1.B', telemetryProps: { feature: '1.b' } },
],
menus: {
[ContributableMenu.GlobalNav]: [{ action: '1.a' }, { action: '1.b' }],
@ -23,8 +23,8 @@ describe('mergeContributions()', () => {
const FIXTURE_CONTRIBUTIONS_2: Evaluated<Contributions> = {
actions: [
{ id: '2.a', command: 'c', title: '2.A' },
{ id: '2.b', command: 'c', title: '2.B' },
{ id: '2.a', command: 'c', title: '2.A', telemetryProps: { feature: '2.a' } },
{ id: '2.b', command: 'c', title: '2.B', telemetryProps: { feature: '2.b' } },
],
menus: {
[ContributableMenu.EditorTitle]: [{ action: '2.a' }, { action: '2.b' }],
@ -32,10 +32,10 @@ describe('mergeContributions()', () => {
}
const FIXTURE_CONTRIBUTIONS_MERGED: Evaluated<Contributions> = {
actions: [
{ id: '1.a', command: 'c', title: '1.A' },
{ id: '1.b', command: 'c', title: '1.B' },
{ id: '2.a', command: 'c', title: '2.A' },
{ id: '2.b', command: 'c', title: '2.B' },
{ id: '1.a', command: 'c', title: '1.A', telemetryProps: { feature: '1.a' } },
{ id: '1.b', command: 'c', title: '1.B', telemetryProps: { feature: '1.b' } },
{ id: '2.a', command: 'c', title: '2.A', telemetryProps: { feature: '2.a' } },
{ id: '2.b', command: 'c', title: '2.B', telemetryProps: { feature: '2.b' } },
],
menus: {
[ContributableMenu.GlobalNav]: [{ action: '1.a' }, { action: '1.b' }],
@ -77,9 +77,9 @@ describe('filterContributions()', () => {
it('handles non-empty contributions', () => {
const expected: Evaluated<Contributions> = {
actions: [
{ id: 'a1', command: 'c' },
{ id: 'a2', command: 'c' },
{ id: 'a3', command: 'c' },
{ id: 'a1', command: 'c', telemetryProps: { feature: 'a1' } },
{ id: 'a2', command: 'c', telemetryProps: { feature: 'a2' } },
{ id: 'a3', command: 'c', telemetryProps: { feature: 'a3' } },
],
menus: {
[ContributableMenu.GlobalNav]: [{ action: 'a1', when: true }, { action: 'a2' }],
@ -88,9 +88,9 @@ describe('filterContributions()', () => {
expect(
filterContributions({
actions: [
{ id: 'a1', command: 'c' },
{ id: 'a2', command: 'c' },
{ id: 'a3', command: 'c' },
{ id: 'a1', command: 'c', telemetryProps: { feature: 'a1' } },
{ id: 'a2', command: 'c', telemetryProps: { feature: 'a2' } },
{ id: 'a3', command: 'c', telemetryProps: { feature: 'a3' } },
],
menus: {
[ContributableMenu.GlobalNav]: [
@ -139,12 +139,14 @@ describe('evaluateContributions()', () => {
iconDescription: parseTemplate('${replaceMe}'),
iconURL: parseTemplate('${replaceMe}'),
},
telemetryProps: { feature: 'a1' },
},
{
id: 'a2',
command: 'c2',
title: parseTemplate('${replaceMe}'),
category: parseTemplate('b'),
telemetryProps: { feature: 'a2' },
},
{
id: 'a3',
@ -155,6 +157,7 @@ describe('evaluateContributions()', () => {
label: parseTemplate('${replaceMe}'),
description: parseTemplate('b'),
},
telemetryProps: { feature: 'a3' },
},
],
}
@ -175,9 +178,17 @@ describe('evaluateContributions()', () => {
iconDescription: 'x',
iconURL: 'x',
},
telemetryProps: { feature: 'a1' },
},
{ id: 'a2', command: 'c2', title: 'x', category: 'b', telemetryProps: { feature: 'a2' } },
{
id: 'a3',
command: 'c3',
title: 'b',
category: 'b',
actionItem: { label: 'x', description: 'b' },
telemetryProps: { feature: 'a3' },
},
{ id: 'a2', command: 'c2', title: 'x', category: 'b' },
{ id: 'a3', command: 'c3', title: 'b', category: 'b', actionItem: { label: 'x', description: 'b' } },
],
}
expect(evaluateContributions(FIXTURE_CONTEXT, input)).toEqual(expected)
@ -191,6 +202,7 @@ describe('evaluateContributions()', () => {
id: 'x',
command: 'c',
commandArguments: ['b', 'x', 'b', 'x'],
telemetryProps: { feature: 'x' },
},
],
}
@ -206,6 +218,7 @@ describe('evaluateContributions()', () => {
parseTemplate('b'),
parseTemplate('${replaceMe}'),
],
telemetryProps: { feature: 'x' },
},
],
})
@ -222,11 +235,14 @@ describe('evaluateContributions()', () => {
actionItem: {
pressed: parse('a'),
},
telemetryProps: { feature: 'a' },
},
],
}
expect(evaluateContributions(FIXTURE_CONTEXT, input)).toEqual({
actions: [{ id: 'a', command: 'c', title: 'a', actionItem: { pressed: true } }],
actions: [
{ id: 'a', command: 'c', title: 'a', actionItem: { pressed: true }, telemetryProps: { feature: 'a' } },
],
})
})
})
@ -234,11 +250,11 @@ describe('evaluateContributions()', () => {
describe('parseContributionExpressions()', () => {
it('should not parse the `id` or `command` values', () => {
const expected: Contributions = {
actions: [{ id: '${replaceMe}', command: '${c}' }],
actions: [{ id: '${replaceMe}', command: '${c}', telemetryProps: { feature: 'a' } }],
}
expect(
parseContributionExpressions({
actions: [{ id: '${replaceMe}', command: '${c}' }],
actions: [{ id: '${replaceMe}', command: '${c}', telemetryProps: { feature: 'a' } }],
})
).toEqual(expected)
})

View File

@ -7,6 +7,7 @@ import { describe, it } from 'vitest'
import type { Contributions } from '@sourcegraph/client-api'
import type { SettingsCascade } from '../../../settings/settings'
import { noOpTelemetryRecorder } from '../../../telemetry'
import type { MainThreadAPI } from '../../contract'
import { pretendRemote } from '../../util'
import { activateExtensions, type ExecutableExtension } from '../activation'
@ -17,8 +18,9 @@ describe('Extension activation', () => {
it('logs events for activated extensions', async () => {
const logEvent = sinon.spy()
const mockMain = pretendRemote<Pick<MainThreadAPI, 'logEvent'>>({
const mockMain = pretendRemote<Pick<MainThreadAPI, 'logEvent' | 'getTelemetryRecorder'>>({
logEvent,
getTelemetryRecorder: () => noOpTelemetryRecorder,
})
const FIXTURE_EXTENSION: ExecutableExtension = {

View File

@ -5,6 +5,7 @@ import type { GraphQLResult } from '@sourcegraph/http-client'
import type { PlatformContext } from '../../platform/context'
import type { Settings, SettingsCascade } from '../../settings/settings'
import type { TelemetryV2Props } from '../../telemetry'
/**
* Represents a location inside a resource, such as a line
@ -377,7 +378,8 @@ export function updateCodeIntelContext(newContext: CodeIntelContext): void {
context = newContext
}
export interface CodeIntelContext extends Pick<PlatformContext, 'requestGraphQL' | 'telemetryService'> {
export interface CodeIntelContext
extends Pick<PlatformContext, 'requestGraphQL' | 'telemetryService' | 'telemetryRecorder'> {
settings: SettingsGetter
}
@ -419,3 +421,7 @@ export function logTelemetryEvent(
): void {
context?.telemetryService?.log(eventName, eventProperties)
}
export function getTelemetryRecorder(): TelemetryV2Props['telemetryRecorder'] | undefined {
return context?.telemetryRecorder
}

View File

@ -7,7 +7,7 @@ import type { LanguageSpec } from './language-specs/language-spec'
import { type Logger, NoopLogger } from './logging'
import { createProviders as createLSIFProviders } from './lsif/providers'
import { createProviders as createSearchProviders } from './search/providers'
import { TelemetryEmitter } from './telemetry'
import { type CodeIntelActions, TelemetryEmitter } from './telemetry'
import { API } from './util/api'
import { asArray, mapArrayish, nonEmpty } from './util/helpers'
import { noopAsyncGenerator, observableFromAsyncIterator } from './util/ix'
@ -465,7 +465,7 @@ function logLocationResults<T extends sourcegraph.Badged<sourcegraph.Location>,
logger,
}: {
provider: string
action: string
action: CodeIntelActions
repo: string
textDocument: sourcegraph.TextDocument
position: sourcegraph.Position
@ -473,11 +473,11 @@ function logLocationResults<T extends sourcegraph.Badged<sourcegraph.Location>,
emitter?: TelemetryEmitter
logger?: Logger
}): void {
emitter?.emitOnce(action)
emitter?.emitOnce(false, action)
// Emit xrepo event if we contain a result from another repository
if (asArray(results).some(location => parseGitURI(location.uri.toString()).repo !== repo)) {
emitter?.emitOnce(action + '.xrepo')
emitter?.emitOnce(true, action)
}
if (logger) {
@ -555,7 +555,7 @@ export function createHoverProvider(
}
}
emitter.emitOnce('lsifHover')
emitter.emitOnce(false, 'lsifHover')
logger?.log({ provider: 'hover', precise: true, ...commonLogFields })
yield badgeHoverResult(
lsifWrapper.hover,
@ -582,7 +582,7 @@ export function createHoverProvider(
continue
}
emitter.emitOnce('searchHover')
emitter.emitOnce(false, 'searchHover')
logger?.log({ provider: 'hover', precise: false, ...commonLogFields })
if (hasPreciseDefinition) {
@ -634,14 +634,14 @@ export function createDocumentHighlightProvider(
for await (const lsifResult of lsifProvider(textDocument, position)) {
if (lsifResult) {
emitter.emitOnce('lsifDocumentHighlight')
emitter.emitOnce(false, 'lsifDocumentHighlight')
yield lsifResult
hasLsifResults = true
}
}
if (!hasLsifResults) {
emitter.emitOnce('searchDocumentHighlight')
emitter.emitOnce(false, 'searchDocumentHighlight')
for await (const searchResult of searchProvider(textDocument, position)) {
if (searchResult) {
yield searchResult

View File

@ -1,5 +1,16 @@
import * as sourcegraph from './api'
export type CodeIntelActions =
| 'lsifHover'
| 'searchHover'
| 'lsifDocumentHighlight'
| 'searchDocumentHighlight'
| 'lsifDefinitions'
| 'searchDefinitions'
| 'lsifReferences'
| 'searchReferences'
| 'lsifImplementations'
/**
* A wrapper around telemetry events. A new instance of this class
* should be instantiated at the start of each action as it handles
@ -33,31 +44,38 @@ export class TelemetryEmitter {
* same action has not yet emitted for this instance. This method
* returns true if an event was emitted and false otherwise.
*/
public emitOnce(action: string, args: object = {}): boolean {
public emitOnce(xrepo: boolean, action: CodeIntelActions, args: object = {}): boolean {
if (this.emitted.has(action)) {
return false
}
this.emitted.add(action)
this.emit(action, args)
this.emit(xrepo, action, args)
return true
}
/**
* Emit a telemetry event with durationMs and languageId attributes.
*/
public emit(action: string, args: object = {}): void {
public emit(xrepo: boolean, action: CodeIntelActions, args: object = {}): void {
if (!this.enabled) {
return
}
try {
sourcegraph.logTelemetryEvent(`codeintel.${action}`, {
sourcegraph.logTelemetryEvent(`codeintel.${action + xrepo ? '.xrepo' : ''}`, {
...args,
durationMs: this.elapsed(),
languageId: this.languageID,
repositoryId: this.repoID,
})
const telemetryRecorder = sourcegraph.getTelemetryRecorder()
telemetryRecorder?.recordEvent(`blob.codeintel${xrepo ? '.xrepo' : ''}`, action, {
metadata: {
durationMs: this.elapsed(),
repositoryId: this.repoID,
},
})
} catch {
// Older version of Sourcegraph may have not registered this
// command, causing the promise to reject. We can safely ignore

View File

@ -17,7 +17,10 @@ import type { PlatformContext } from '../platform/context'
* documentation.
*/
export function registerBuiltinClientCommands(
context: Pick<PlatformContext, 'requestGraphQL' | 'telemetryService' | 'settings' | 'updateSettings'>,
context: Pick<
PlatformContext,
'requestGraphQL' | 'telemetryService' | 'telemetryRecorder' | 'settings' | 'updateSettings'
>,
extensionHost: Remote<FlatExtensionHostAPI>,
registerCommand: (entryToRegister: CommandEntry) => Unsubscribable
): Unsubscribable {
@ -116,6 +119,8 @@ export function registerBuiltinClientCommands(
if (context.telemetryService) {
context.telemetryService.log(eventName, eventProperties)
}
// TODO (dadlerj): cannot log telemetry v2 events here as the name isn't a known string.
// TBD whether this is needed.
return Promise.resolve()
},
})

View File

@ -12,9 +12,9 @@ describe('getContributedActionItems', () => {
getContributedActionItems(
{
actions: [
{ id: 'a', command: 'a', title: 'ta', description: 'da' },
{ id: 'b', command: 'b', title: 'tb', description: 'db' },
{ id: 'c', command: 'c', title: 'tc', description: 'dc' },
{ id: 'a', command: 'a', title: 'ta', description: 'da', telemetryProps: { feature: 'a' } },
{ id: 'b', command: 'b', title: 'tb', description: 'db', telemetryProps: { feature: 'b' } },
{ id: 'c', command: 'c', title: 'tc', description: 'dc', telemetryProps: { feature: 'c' } },
],
menus: {
'editor/title': [{ action: 'b', alt: 'c' }, { action: 'a' }],
@ -24,13 +24,13 @@ describe('getContributedActionItems', () => {
)
).toEqual([
{
action: { id: 'b', command: 'b', title: 'tb', description: 'db' },
action: { id: 'b', command: 'b', title: 'tb', description: 'db', telemetryProps: { feature: 'b' } },
active: true,
altAction: { id: 'c', command: 'c', title: 'tc', description: 'dc' },
altAction: { id: 'c', command: 'c', title: 'tc', description: 'dc', telemetryProps: { feature: 'c' } },
disabledWhen: false,
},
{
action: { id: 'a', command: 'a', title: 'ta', description: 'da' },
action: { id: 'a', command: 'a', title: 'ta', description: 'da', telemetryProps: { feature: 'a' } },
active: true,
altAction: undefined,
disabledWhen: false,

View File

@ -25,6 +25,7 @@ export function createController(
| 'requestGraphQL'
| 'getStaticExtensions'
| 'telemetryService'
| 'telemetryRecorder'
| 'clientApplication'
| 'sourcegraphURL'
| 'createExtensionHost'

View File

@ -5,6 +5,7 @@ import { MarkupKind } from '@sourcegraph/extension-api-classes'
import type { ActionItemAction } from '../actions/ActionItem'
import type { MarkupContent, Badged, AggregableBadge } from '../codeintel/legacy-extensions/api'
import { EMPTY_SETTINGS_CASCADE, type SettingsCascadeProps } from '../settings/settings'
import { noOpTelemetryRecorder } from '../telemetry'
import { NOOP_TELEMETRY_SERVICE } from '../telemetry/telemetryService'
import type { HoverOverlayProps } from './HoverOverlay'
@ -15,6 +16,7 @@ const NOOP_EXTENSIONS_CONTROLLER = { executeCommand: () => Promise.resolve() }
export const commonProps = (): HoverOverlayProps & SettingsCascadeProps => ({
location: history.location,
telemetryService: NOOP_TELEMETRY_SERVICE,
telemetryRecorder: noOpTelemetryRecorder,
extensionsController: NOOP_EXTENSIONS_CONTROLLER,
overlayPosition: { top: 16, left: 16 },
settingsCascade: EMPTY_SETTINGS_CASCADE,
@ -40,6 +42,9 @@ export const FIXTURE_ACTIONS: ActionItemAction[] = [
title: 'Go to definition',
command: 'open',
commandArguments: ['/github.com/sourcegraph/codeintellify/-/blob/src/hoverifier.ts#L57:1'],
telemetryProps: {
feature: 'blob.goToDefinition.preloaded',
},
},
active: true,
},
@ -49,6 +54,9 @@ export const FIXTURE_ACTIONS: ActionItemAction[] = [
title: 'Find references',
command: 'open',
commandArguments: ['/github.com/sourcegraph/codeintellify/-/blob/src/hoverifier.ts?tab=references#L57:18'],
telemetryProps: {
feature: 'blob.findReferences',
},
},
active: true,
},

View File

@ -6,6 +6,7 @@ import { describe, expect, test } from 'vitest'
import { subtypeOf } from '@sourcegraph/common'
import { MarkupKind } from '@sourcegraph/extension-api-classes'
import { noOpTelemetryRecorder } from '../telemetry'
import { NOOP_TELEMETRY_SERVICE } from '../telemetry/telemetryService'
import { HoverOverlay, type HoverOverlayProps } from './HoverOverlay'
@ -17,6 +18,7 @@ describe('HoverOverlay', () => {
const commonProps = subtypeOf<HoverOverlayProps>()({
location: history.location,
telemetryService: NOOP_TELEMETRY_SERVICE,
telemetryRecorder: noOpTelemetryRecorder,
extensionsController: NOOP_EXTENSIONS_CONTROLLER,
platformContext: NOOP_PLATFORM_CONTEXT,
hoveredToken: { repoName: 'r', commitID: 'c', revision: 'v', filePath: 'f', line: 1, character: 2 },
@ -62,7 +64,12 @@ describe('HoverOverlay', () => {
render(
<HoverOverlay
{...commonProps}
actionsOrError={[{ action: { id: 'a', command: 'c', title: 'Some title' }, active: true }]}
actionsOrError={[
{
action: { id: 'a', command: 'c', title: 'Some title', telemetryProps: { feature: 'test' } },
active: true,
},
]}
/>
).asFragment()
).toMatchSnapshot()
@ -100,7 +107,9 @@ describe('HoverOverlay', () => {
render(
<HoverOverlay
{...commonProps}
actionsOrError={[{ action: { id: 'a', command: 'c' }, active: true }]}
actionsOrError={[
{ action: { id: 'a', command: 'c', telemetryProps: { feature: 'a' } }, active: true },
]}
hoverOrError={{ contents: [{ kind: MarkupKind.Markdown, value: 'v' }] }}
/>
).asFragment()
@ -112,7 +121,9 @@ describe('HoverOverlay', () => {
render(
<HoverOverlay
{...commonProps}
actionsOrError={[{ action: { id: 'a', command: 'c' }, active: true }]}
actionsOrError={[
{ action: { id: 'a', command: 'c', telemetryProps: { feature: 'a' } }, active: true },
]}
hoverOrError="loading"
/>
).asFragment()
@ -172,7 +183,9 @@ describe('HoverOverlay', () => {
render(
<HoverOverlay
{...commonProps}
actionsOrError={[{ action: { id: 'a', command: 'c' }, active: true }]}
actionsOrError={[
{ action: { id: 'a', command: 'c', telemetryProps: { feature: 'a' } }, active: true },
]}
hoverOrError={{ message: 'm', name: 'c' }}
/>
).asFragment()

View File

@ -7,6 +7,7 @@ import { isErrorLike, sanitizeClass } from '@sourcegraph/common'
import { Card, Icon, Button } from '@sourcegraph/wildcard'
import { ActionItem, type ActionItemComponentProps } from '../actions/ActionItem'
import type { TelemetryV2Props } from '../telemetry'
import type { TelemetryProps } from '../telemetry/telemetryService'
import { CopyLinkIcon } from './CopyLinkIcon'
@ -45,7 +46,8 @@ export interface HoverOverlayProps
extends HoverOverlayBaseProps,
ActionItemComponentProps,
HoverOverlayClassProps,
TelemetryProps {
TelemetryProps,
TelemetryV2Props {
/** A ref callback to get the root overlay element. Use this to calculate the position. */
hoverRef?: React.Ref<HTMLDivElement>
@ -92,6 +94,7 @@ export const HoverOverlay: React.FunctionComponent<React.PropsWithChildren<Hover
overlayPosition,
actionsOrError,
telemetryService,
telemetryRecorder,
extensionsController,
pinOptions,
location,
@ -188,6 +191,7 @@ export const HoverOverlay: React.FunctionComponent<React.PropsWithChildren<Hover
disabledDuringExecution={true}
showLoadingSpinnerDuringExecution={true}
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
extensionsController={extensionsController}
location={location}
actionItemStyleProps={actionItemStyleProps}

View File

@ -379,6 +379,9 @@ export function registerHoverContributions({
'${json(hoverPosition)}',
/* eslint-enable no-template-curly-in-string */
],
telemetryProps: {
feature: 'blob.goToDefinition',
},
},
{
// This action is used when preloading the definition succeeded and at least 1
@ -389,6 +392,9 @@ export function registerHoverContributions({
command: 'open',
// eslint-disable-next-line no-template-curly-in-string
commandArguments: ['${goToDefinition.url}'],
telemetryProps: {
feature: 'blob.goToDefinition.preloaded',
},
},
],
menus: {
@ -479,6 +485,9 @@ export function registerHoverContributions({
command: 'open',
// eslint-disable-next-line no-template-curly-in-string
commandArguments: ['${findReferences.url}'],
telemetryProps: {
feature: 'blob.findReferences',
},
},
],
menus: {
@ -518,6 +527,10 @@ export function registerHoverContributions({
],
id: 'findImplementations_' + spec.languageID,
title: 'Find implementations',
telemetryProps: {
feature: 'blob.findImplementations',
privateMetadata: { languageID: spec.languageID },
},
})),
],
menus: {

View File

@ -17,10 +17,13 @@ const isEmptyHover = ({
// Log telemetry event on mount and once per new hover position
export function useLogTelemetryEvent(props: HoverOverlayProps): void {
const { telemetryService, hoveredToken } = props
const { telemetryService, telemetryRecorder, hoveredToken } = props
const previousPropsReference = useRef(props)
const logTelemetryEvent = (): void => telemetryService.log('hover')
const logTelemetryEvent = (): void => {
telemetryService.log('hover')
telemetryRecorder.recordEvent('blob', 'hover')
}
// Log a telemetry event on component mount once, so we don't care about dependency updates.
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -3,7 +3,7 @@ import React, { createContext } from 'react'
import type { StoreApi, UseBoundStore } from 'zustand'
import type { SearchPatternType } from '../graphql-operations'
import { TelemetryV2Props } from '../telemetry'
import type { TelemetryV2Props } from '../telemetry'
import { type QueryState, type SubmitSearchParameters, toggleSubquery } from './helpers'
import type { FilterType } from './query/filters'

View File

@ -9,6 +9,7 @@ import { type InitData, startExtensionHost } from '../api/extension/extensionHos
import type { WorkspaceRootWithMetadata } from '../api/extension/extensionHostApi'
import type { TextDocumentData, ViewerData } from '../api/viewerTypes'
import type { EndpointPair, PlatformContext } from '../platform/context'
import { noOpTelemetryRecorder } from '../telemetry'
export function assertToJSON(a: any, expected: any): void {
const raw = JSON.stringify(a)
@ -38,7 +39,12 @@ const FIXTURE_INIT_DATA: TestInitData = {
interface Mocks
extends Pick<
PlatformContext,
'settings' | 'updateSettings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
| 'settings'
| 'updateSettings'
| 'getGraphQLClient'
| 'requestGraphQL'
| 'clientApplication'
| 'telemetryRecorder'
> {}
const NOOP_MOCKS: Mocks = {
@ -47,6 +53,7 @@ const NOOP_MOCKS: Mocks = {
getGraphQLClient: () => Promise.reject(new Error('Mocks#getGraphQLClient not implemented')),
requestGraphQL: () => throwError(() => new Error('Mocks#queryGraphQL not implemented')),
clientApplication: 'sourcegraph',
telemetryRecorder: noOpTelemetryRecorder,
}
/**

View File

@ -8,6 +8,8 @@
import { from } from 'rxjs'
import { writable } from 'svelte/store'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { goto, preloadData, afterNavigate } from '$app/navigation'
import { page } from '$app/stores'
import type { ScrollSnapshot } from '$lib/codemirror/utils'
@ -92,6 +94,7 @@
requestGraphQL(options) {
return from(graphQLClient.query(options.request, options.variables).then(toGraphQLResult))
},
telemetryRecorder: noOpTelemetryRecorder,
})
: null

View File

@ -13,6 +13,8 @@
import { mdiClose } from '@mdi/js'
import { from } from 'rxjs'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import CodeMirrorBlob from '$lib/CodeMirrorBlob.svelte'
import { isErrorLike } from '$lib/common'
import { getGraphQLClient, mapOrThrow, toGraphQLResult } from '$lib/graphql'
@ -49,6 +51,7 @@
requestGraphQL(options) {
return from(client.query(options.request, options.variables).then(toGraphQLResult))
},
telemetryRecorder: noOpTelemetryRecorder,
})
$: blobStore = toReadable(

View File

@ -45,8 +45,8 @@
"@sourcegraph/observability-client": "workspace:*",
"@sourcegraph/schema": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/telemetry": "^0.16.0",
"@sourcegraph/wildcard": "workspace:*",
"mermaid": "^10.9.1"
}
}
}

View File

@ -22,7 +22,7 @@ import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/u
import { Shortcut } from '@sourcegraph/shared/src/react-shortcuts'
import { useSettings } from '@sourcegraph/shared/src/settings/settings'
import type { TemporarySettingsSchema } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings'
import { type TelemetryV2Props, noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { type TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import { codeCopiedEvent } from '@sourcegraph/shared/src/tracking/event-log-creators'
@ -313,6 +313,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
)
const codeIntelExtension = useCodeIntelExtension(
telemetryService,
telemetryRecorder,
{
repoName: blobInfo.repoName,
filePath: blobInfo.filePath,
@ -355,8 +356,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
codeFoldingExtension(),
isCodyEnabledForFile
? codyWidgetExtension(
// TODO: replace with real telemetryRecorder
noOpTelemetryRecorder,
telemetryRecorder,
editorRef.current
? new CodeMirrorEditor({
view: editorRef.current,
@ -538,6 +538,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = props => {
function useCodeIntelExtension(
telemetryService: TelemetryProps['telemetryService'],
telemetryRecorder: TelemetryV2Props['telemetryRecorder'],
{
repoName,
filePath,
@ -559,9 +560,10 @@ function useCodeIntelExtension(
settings: name => settings[name],
requestGraphQL: requestGraphQLAdapter(apolloClient),
telemetryService,
telemetryRecorder,
})
: null,
[settings, apolloClient, telemetryService]
[settings, apolloClient, telemetryService, telemetryRecorder]
)
useEffect(() => {

View File

@ -340,6 +340,9 @@ export class CodeIntelAPIAdapter {
disabledTitle:
definition.type === 'none' ? 'No definition found' : 'You are at the definition',
command: 'open',
telemetryProps: {
feature: 'blob.goToDefinition',
},
},
})
} else if (definition.type === 'initial') {
@ -350,6 +353,9 @@ export class CodeIntelAPIAdapter {
title: 'Go to definition',
command: 'invokeFunction-new',
commandArguments: [() => this.goToDefinitionAtOccurrence(view, occurrence)],
telemetryProps: {
feature: 'blob.goToDefinition',
},
},
})
} else {
@ -377,6 +383,9 @@ export class CodeIntelAPIAdapter {
return false
},
],
telemetryProps: {
feature: 'blob.goToDefinition',
},
},
})
}
@ -389,6 +398,9 @@ export class CodeIntelAPIAdapter {
commandArguments: [
() => this.config.openReferences(view, this.config.documentInfo, occurrence),
],
telemetryProps: {
feature: 'blob.findReferences',
},
},
})
@ -402,6 +414,9 @@ export class CodeIntelAPIAdapter {
commandArguments: [
() => this.config.openImplementations(view, this.config.documentInfo, occurrence),
],
telemetryProps: {
feature: 'blob.findImplementations',
},
},
})
}
@ -412,6 +427,9 @@ export class CodeIntelAPIAdapter {
title: '?', // special marker for the MDI "Help" icon.
description: `Go to definition with ${modifierClickDescription}, long-click, or by pressing Enter with the keyboard. Display this popover by pressing Space with the keyboard.`,
command: '',
telemetryProps: {
feature: 'blob.goToDefinition.help',
},
},
})
return actions

View File

@ -128,6 +128,7 @@ export class HovercardView implements TooltipView {
location={props.location}
onHoverShown={props.onHoverShown}
telemetryService={props.telemetryService}
telemetryRecorder={props.telemetryRecorder}
extensionsController={NOOP_EXTENSION_CONTROLLER}
// Hover props
actionsOrError={actionsOrError}

View File

@ -35,7 +35,7 @@ import {
type StreamSearchOptions,
} from '@sourcegraph/shared/src/search/stream'
import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE, type TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
import { Button, H2, H4, Icon, Link, Panel, useLocalStorage, useScrollManager } from '@sourcegraph/wildcard'

View File

@ -1391,8 +1391,8 @@ importers:
specifier: workspace:*
version: link:../http-client
'@sourcegraph/telemetry':
specifier: ^0.11.0
version: 0.11.0
specifier: ^0.16.0
version: 0.16.0
'@sourcegraph/template-parser':
specifier: workspace:*
version: link:../template-parser
@ -1503,8 +1503,8 @@ importers:
specifier: workspace:*
version: link:../shared
'@sourcegraph/telemetry':
specifier: ^0.11.0
version: 0.11.0
specifier: ^0.16.0
version: 0.16.0
'@sourcegraph/wildcard':
specifier: workspace:*
version: link:../wildcard
@ -8616,8 +8616,8 @@ packages:
stylelint: 14.3.0
dev: true
/@sourcegraph/telemetry@0.11.0:
resolution: {integrity: sha512-cOlkCwX3V5lVJO/F8w7VauhMZob3PrMbE4DApTKwasTMwm4+OxtT6+afC5wJTwZwNWc31V83sNjV2nBkEtFPZg==}
/@sourcegraph/telemetry@0.16.0:
resolution: {integrity: sha512-/LTbGWwscy/iNOYc0JS2Sl5Q21WOaiIhABlGynwJLMfPYy0YoJQfEtrCEGTSYWY735dSdUBW3wcTgCOCz8EEjA==}
dependencies:
rxjs: 7.8.1
dev: false