[Backport 5.2] client/web: add telemetry v2 client (#57939) (#58085)

Closes https://github.com/sourcegraph/sourcegraph/issues/56920
This commit is contained in:
Robert Lin 2023-11-03 14:14:13 -07:00 committed by GitHub
parent ca5b448a34
commit c484b3ef4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 429 additions and 42 deletions

View File

@ -6,6 +6,7 @@ import { isHTTPAuthError } from '@sourcegraph/http-client'
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
import { mutateSettings, updateSettings } from '@sourcegraph/shared/src/settings/edit'
import { EMPTY_SETTINGS_CASCADE, gqlToCascade, type SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
import { NoOpTelemetryRecorderProvider } from '@sourcegraph/shared/src/telemetry'
import { toPrettyBlobURL } from '@sourcegraph/shared/src/util/url'
import { createGraphQLHelpers } from '../backend/requestGraphQl'
@ -128,6 +129,12 @@ export function createPlatformContext(
sourcegraphURL,
clientApplication: 'other',
getStaticExtensions: () => getInlineExtensions(assetsURL),
/**
* @todo Not yet implemented!
*/
telemetryRecorder: new NoOpTelemetryRecorderProvider({
errorOnRecord: true, // should not be used at all
}).getRecorder(),
}
return context
}

View File

@ -323,6 +323,7 @@ ts_project(
"src/symbols/SymbolKind.tsx",
"src/symbols/SymbolTag.tsx",
"src/symbols/symbolIcons.ts",
"src/telemetry/index.ts",
"src/telemetry/telemetryService.ts",
"src/theme.ts",
"src/tracking/event-log-creators.ts",
@ -346,6 +347,7 @@ ts_project(
":node_modules/@sourcegraph/common",
":node_modules/@sourcegraph/extension-api-types",
":node_modules/@sourcegraph/http-client",
":node_modules/@sourcegraph/telemetry",
":node_modules/@sourcegraph/template-parser",
":node_modules/@sourcegraph/wildcard",
":node_modules/sourcegraph",

View File

@ -13,18 +13,19 @@
"watch-schema": "gulp watchSchema"
},
"devDependencies": {
"@sourcegraph/testing": "workspace:*",
"@sourcegraph/build-config": "workspace:*",
"@sourcegraph/extension-api-types": "workspace:*",
"@sourcegraph/testing": "workspace:*",
"sourcegraph": "workspace:*"
},
"dependencies": {
"@sourcegraph/wildcard": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/client-api": "workspace:*",
"@sourcegraph/codeintellify": "workspace:*",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/template-parser": "workspace:*",
"@sourcegraph/codeintellify": "workspace:*"
"@sourcegraph/wildcard": "workspace:*"
},
"sideEffects": true
}
}

View File

@ -10,6 +10,7 @@ import type { SettingsEdit } from '../api/client/services/settings'
import type { ExecutableExtension } from '../api/extension/activation'
import type { Scalars } from '../graphql-operations'
import type { Settings, SettingsCascadeOrError } from '../settings/settings'
import type { TelemetryRecorder } from '../telemetry'
import type { TelemetryService } from '../telemetry/telemetryService'
import type { FileSpec, UIPositionSpec, RawRepoSpec, RepoSpec, RevisionSpec, ViewStateSpec } from '../util/url'
@ -180,9 +181,20 @@ export interface PlatformContext {
/**
* A telemetry service implementation to log events.
* Optional because it's currently only used in the web app platform.
*
* @deprecated Use 'telemetryRecorder' instead.
*/
telemetryService?: TelemetryService
/**
* Telemetry recorder for the new telemetry framework, superseding
* 'telemetryService' and 'logEvent' variants. Learn more here:
* https://docs.sourcegraph.com/dev/background-information/telemetry
*
* It is backed by a '@sourcegraph/telemetry' implementation.
*/
telemetryRecorder: TelemetryRecorder
/**
* If this is a function that returns a Subscribable of executable extensions,
* the extension host will not activate any other settings (e.g. extensions from user settings)

View File

@ -0,0 +1,61 @@
import {
TelemetryRecorderProvider as BaseTelemetryRecorderProvider,
NoOpTelemetryExporter,
type TelemetryProcessor,
CallbackTelemetryProcessor,
} from '@sourcegraph/telemetry'
/**
* TelemetryRecorderProvider type used in Sourcegraph clients.
*/
export type TelemetryRecorderProvider = typeof noOptelemetryRecorderProvider
/**
* TelemetryRecorder type used in Sourcegraph clients.
*/
export type TelemetryRecorder = typeof noOpTelemetryRecorder
/**
* Events accept billing metadata for ease of categorization in analytics
* pipelines - this type enumerates known categories.
*/
export type BillingCategory = 'exampleBillingCategory'
/**
* Events accept billing metadata for ease of categorization in analytics
* pipelines - this type enumerates known products.
*/
export type BillingProduct = 'exampleBillingProduct'
/**
* Props interface that can be extended by React components depending on the
* new telemetry framework: https://docs.sourcegraph.com/dev/background-information/telemetry
* These properties are part of {@link PlatformContext}.
*/
export interface TelemetryV2Props {
/**
* Telemetry recorder for the new telemetry framework, superseding
* 'telemetryService' and 'logEvent' variants. Learn more here:
* https://docs.sourcegraph.com/dev/background-information/telemetry
*
* It is backed by a '@sourcegraph/telemetry' implementation.
*/
telemetryRecorder: TelemetryRecorder
}
export class NoOpTelemetryRecorderProvider extends BaseTelemetryRecorderProvider<BillingCategory, BillingProduct> {
constructor(opts?: { errorOnRecord?: boolean }) {
const processors: TelemetryProcessor[] = []
if (opts?.errorOnRecord) {
processors.push(
new CallbackTelemetryProcessor(() => {
throw new Error('telemetry: unexpected use of no-op telemetry recorder')
})
)
}
super({ client: '' }, new NoOpTelemetryExporter(), processors)
}
}
export const noOptelemetryRecorderProvider = new NoOpTelemetryRecorderProvider()
export const noOpTelemetryRecorder = noOptelemetryRecorderProvider.getRecorder()

View File

@ -1,19 +1,31 @@
/**
* Props interface that can be extended by React components depending on the TelemetryService.
* These properties are part of {@link PlatformContext}.
*
* @deprecated Use TelemetryV2Props for a '@sourcegraph/telemetry' implementation
* instead.
*/
export interface TelemetryProps {
/**
* A telemetry service implementation to log events.
*
* @deprecated Use telemetryRecorder instead from TelemetryV2Props, if it is
* non-null (i.e. if the new SDK is available for the platform).
*/
telemetryService: TelemetryService
}
/**
* The telemetry service logs events.
*
* @deprecated Use a '@sourcegraph/telemetry' implementation instead.
*/
export interface TelemetryService {
/**
* Log an event (by sending it to the server).
*
* @deprecated Use a '@sourcegraph/telemetry' implementation instead where
* available.
*/
log(eventName: string, eventProperties?: any, publicArgument?: any): void
/**
@ -25,11 +37,14 @@ export interface TelemetryService {
/**
* Log a pageview event (by sending it to the server).
* Adheres to the new event naming policy
*
* @deprecated Use a '@sourcegraph/telemetry' implementation instead.
*/
logPageView(eventName: string, eventProperties?: any, publicArgument?: any): void
/**
* Listen for event logs
*
* @deprecated Use a '@sourcegraph/telemetry' implementation instead.
* @returns a cleanup/removeEventListener function
*/
addEventLogListener?(callback: (eventName: string) => void): () => void

View File

@ -1680,6 +1680,8 @@ ts_project(
"src/team/new/NewTeamPage.tsx",
"src/team/new/team-select/ParentTeamSelect.tsx",
"src/team/new/team-select/backend.ts",
"src/telemetry/apolloTelemetryExporter.ts",
"src/telemetry/index.ts",
"src/tour/GettingStartedTour.tsx",
"src/tour/GettingStartedTourSetup.tsx",
"src/tour/components/ItemPicker.tsx",
@ -1774,6 +1776,7 @@ ts_project(
":node_modules/@sourcegraph/http-client",
":node_modules/@sourcegraph/observability-client",
":node_modules/@sourcegraph/shared",
":node_modules/@sourcegraph/telemetry",
":node_modules/@sourcegraph/wildcard",
"//:node_modules/@apollo/client",
"//:node_modules/@codemirror/commands",

View File

@ -23,8 +23,10 @@ const config = {
* Note: Temporary increase from 400kb.
* Primary cause is due to multiple ongoing migrations that mean we are duplicating similar dependencies.
* Issue to track: https://github.com/sourcegraph/sourcegraph/issues/37845
*
* Note: Increased again from 500kb to get backport in.
*/
maxSize: '500kb',
maxSize: '600kb',
compression: 'none',
},
{

View File

@ -50,6 +50,7 @@
"@sourcegraph/observability-client": "workspace:*",
"@sourcegraph/schema": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/wildcard": "workspace:*"
}
}
}

View File

@ -37,6 +37,7 @@ import {
} from '@sourcegraph/shared/src/settings/settings'
import { TemporarySettingsProvider } from '@sourcegraph/shared/src/settings/temporary/TemporarySettingsProvider'
import { TemporarySettingsStorage } from '@sourcegraph/shared/src/settings/temporary/TemporarySettingsStorage'
import { NoOpTelemetryRecorderProvider } from '@sourcegraph/shared/src/telemetry'
import { WildcardThemeContext, type WildcardTheme } from '@sourcegraph/wildcard'
import { authenticatedUser as authenticatedUserSubject, type AuthenticatedUser, authenticatedUserValue } from './auth'
@ -54,6 +55,7 @@ import { SearchResultsCacheProvider } from './search/results/SearchResultsCacheP
import { GLOBAL_SEARCH_CONTEXT_SPEC } from './SearchQueryStateObserver'
import type { StaticAppConfig } from './staticAppConfig'
import { setQueryStateFromSettings, useNavbarQueryState } from './stores'
import { TelemetryRecorderProvider } from './telemetry'
import { eventLogger } from './tracking/eventLogger'
import { UserSessionStores } from './UserSessionStores'
import { siteSubjectNoAdmin, viewerSubjectFromSettings } from './util/settings'
@ -76,6 +78,8 @@ interface LegacySourcegraphWebAppState extends SettingsCascadeProps {
viewerSubject: LegacyLayoutProps['viewerSubject']
selectedSearchContextSpec?: string
platformContext: PlatformContext
}
const WILDCARD_THEME: WildcardTheme = {
@ -87,12 +91,18 @@ const WILDCARD_THEME: WildcardTheme = {
*/
export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, LegacySourcegraphWebAppState> {
private readonly subscriptions = new Subscription()
private readonly platformContext: PlatformContext = createPlatformContext()
private readonly extensionsController: ExtensionsController | null = createNoopController(this.platformContext)
private readonly extensionsController: ExtensionsController | null
constructor(props: StaticAppConfig) {
super(props)
const basePlatformContext = createPlatformContext({
telemetryRecorderProvider: new NoOpTelemetryRecorderProvider({
errorOnRecord: true, // this will be replaced on render()
}),
})
this.extensionsController = createNoopController(basePlatformContext)
if (this.extensionsController !== null) {
this.subscriptions.add(this.extensionsController)
}
@ -101,6 +111,7 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
authenticatedUser: authenticatedUserValue,
settingsCascade: EMPTY_SETTINGS_CASCADE,
viewerSubject: siteSubjectNoAdmin(),
platformContext: basePlatformContext,
}
}
@ -112,12 +123,23 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
getWebGraphQLClient()
.then(graphqlClient => {
// Create real telemetry recorder provider
const telemetryRecorderProvider = new TelemetryRecorderProvider(graphqlClient, {
enableBuffering: true,
})
this.subscriptions.add(telemetryRecorderProvider)
// Override the no-op telemetryRecorder from initialization
const { platformContext } = this.state
platformContext.telemetryRecorder = telemetryRecorderProvider.getRecorder()
this.setState({
graphqlClient,
temporarySettingsStorage: new TemporarySettingsStorage(
graphqlClient,
window.context.isAuthenticatedUser
),
platformContext,
})
})
.catch(error => {
@ -126,7 +148,7 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
this.subscriptions.add(
combineLatest([
from(this.platformContext.settings),
from(this.state.platformContext.settings),
// Start with `undefined` while we don't know if the viewer is authenticated or not.
authenticatedUserSubject,
]).subscribe(([settingsCascade, authenticatedUser]) => {
@ -196,7 +218,7 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
notebooksEnabled: this.props.notebooksEnabled,
codeMonitoringEnabled: this.props.codeMonitoringEnabled,
searchAggregationEnabled: this.props.searchAggregationEnabled,
platformContext: this.platformContext,
platformContext: this.state.platformContext,
authenticatedUser,
viewerSubject: this.state.viewerSubject,
settingsCascade: this.state.settingsCascade,
@ -279,7 +301,7 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
this.subscriptions.add(
isSearchContextSpecAvailable({
spec,
platformContext: this.platformContext,
platformContext: this.state.platformContext,
}).subscribe(isAvailable => {
if (isAvailable) {
this.setSelectedSearchContextSpecWithNoChecks(spec)
@ -300,7 +322,7 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
}
this.subscriptions.add(
getDefaultSearchContextSpec({ platformContext: this.platformContext }).subscribe(spec => {
getDefaultSearchContextSpec({ platformContext: this.state.platformContext }).subscribe(spec => {
// Fall back to global if no default is returned.
this.setSelectedSearchContextSpecWithNoChecks(spec || GLOBAL_SEARCH_CONTEXT_SPEC)
})
@ -327,5 +349,5 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
parameters: FetchFileParameters,
force?: boolean | undefined
): Observable<string[][]> =>
fetchHighlightedFileLineRanges({ ...parameters, platformContext: this.platformContext }, force)
fetchHighlightedFileLineRanges({ ...parameters, platformContext: this.state.platformContext }, force)
}

View File

@ -44,6 +44,7 @@ import { setQueryStateFromSettings, useNavbarQueryState } from './stores'
import type { AppShellInit } from './storm/app-shell-init'
import { Layout } from './storm/pages/LayoutPage/LayoutPage'
import { loader } from './storm/pages/LayoutPage/LayoutPage.loader'
import { TelemetryRecorderProvider } from './telemetry'
import { UserSessionStores } from './UserSessionStores'
import { siteSubjectNoAdmin, viewerSubjectFromSettings } from './util/settings'
@ -97,7 +98,6 @@ const suspenseCache = new SuspenseCache()
*
* Most of the dynamic values in the `SourcegraphWebApp` depend on this observable.
*/
const platformContext = createPlatformContext()
interface SourcegraphWebAppProps extends StaticAppConfig, AppShellInit {}
@ -106,6 +106,10 @@ export const SourcegraphWebApp: FC<SourcegraphWebAppProps> = props => {
const [subscriptions] = useState(() => new Subscription())
const telemetryRecorderProvider = new TelemetryRecorderProvider(graphqlClient, { enableBuffering: true })
subscriptions.add(telemetryRecorderProvider)
const platformContext = createPlatformContext({ telemetryRecorderProvider })
const [resolvedAuthenticatedUser, setResolvedAuthenticatedUser] = useState<AuthenticatedUser | null>(
authenticatedUserValue
)
@ -146,7 +150,7 @@ export const SourcegraphWebApp: FC<SourcegraphWebAppProps> = props => {
setSelectedSearchContextSpecWithNoChecks(spec || GLOBAL_SEARCH_CONTEXT_SPEC)
})
)
}, [props.searchContextsEnabled, setSelectedSearchContextSpecWithNoChecks, subscriptions])
}, [props.searchContextsEnabled, setSelectedSearchContextSpecWithNoChecks, subscriptions, platformContext])
const setSelectedSearchContextSpec = useCallback(
(spec: string): void => {
@ -183,6 +187,7 @@ export const SourcegraphWebApp: FC<SourcegraphWebAppProps> = props => {
setSelectedSearchContextSpecToDefault,
setSelectedSearchContextSpecWithNoChecks,
subscriptions,
platformContext,
]
)

View File

@ -18,6 +18,7 @@ import {
import '../../SourcegraphWebApp.scss'
import { createPlatformContext } from '../../platform/context'
import { TelemetryRecorderProvider } from '../../telemetry'
import { OpenNewTabAnchorLink } from './OpenNewTabAnchorLink'
@ -61,7 +62,16 @@ export const EmbeddedWebApp: FC<Props> = ({ graphqlClient }) => {
)
}, [setThemeSetting])
const platformContext = useMemo(() => createPlatformContext(), [])
const telemetryRecorderProvider = useMemo(
() => new TelemetryRecorderProvider(graphqlClient, { enableBuffering: true }),
[graphqlClient]
)
useEffect(() => () => telemetryRecorderProvider.unsubscribe(), [telemetryRecorderProvider]) // unsubscribe on unmount
const platformContext = useMemo(
() => createPlatformContext({ telemetryRecorderProvider }),
[telemetryRecorderProvider]
)
// 🚨 SECURITY: The `EmbeddedWebApp` is intended to be embedded into 3rd party sites where we do not have total control.
// That is why it is essential to be mindful when adding new routes that may be vulnerable to clickjacking or similar exploits.

View File

@ -4,6 +4,7 @@ import { mdiChevronDown, mdiInformationOutline } from '@mdi/js'
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { UserAvatar } from '@sourcegraph/shared/src/components/UserAvatar'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
Container,
@ -49,14 +50,18 @@ import styles from './RepoSettingsPermissionsPage.module.scss'
type IUser = INode['user']
export interface RepoSettingsPermissionsPageProps extends TelemetryProps {
export interface RepoSettingsPermissionsPageProps extends TelemetryProps, TelemetryV2Props {
repo: SettingsAreaRepositoryFields
}
/**
* The repository settings permissions page.
*/
export const RepoSettingsPermissionsPage: FC<RepoSettingsPermissionsPageProps> = ({ repo, telemetryService }) => {
export const RepoSettingsPermissionsPage: FC<RepoSettingsPermissionsPageProps> = ({
repo,
telemetryService,
telemetryRecorder,
}) => {
useEffect(() => eventLogger.logViewEvent('RepoSettingsPermissions'))
const [{ query }, setSearchQuery] = useURLSyncedState({ query: '' })
@ -170,7 +175,12 @@ export const RepoSettingsPermissionsPage: FC<RepoSettingsPermissionsPageProps> =
className="my-3 pt-3"
/>
<Container className="mb-3">
<PermissionsSyncJobsTable telemetryService={telemetryService} minimal={true} repoID={repo.id} />
<PermissionsSyncJobsTable
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
minimal={true}
repoID={repo.id}
/>
</Container>
<PageHeader
headingElement="h2"

View File

@ -1,6 +1,7 @@
import sinon from 'sinon'
import { getDocumentNode } from '@sourcegraph/http-client'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockedTestProvider, waitForNextApolloResponse } from '@sourcegraph/shared/src/testing/apollo'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
@ -81,6 +82,7 @@ describe('UserSettingsPermissionsPage', () => {
<UserSettingsPermissionsPage
user={{ id: gqlUserID, username: 'alice' }}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
/>
</MockedTestProvider>,
{}
@ -132,6 +134,7 @@ describe('UserSettingsPermissionsPage', () => {
<UserSettingsPermissionsPage
user={{ id: gqlUserID, username: 'alice' }}
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
/>
</MockedTestProvider>,
{}

View File

@ -4,6 +4,7 @@ import { mdiInformationOutline } from '@mdi/js'
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { RepoLink } from '@sourcegraph/shared/src/components/RepoLink'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
Container,
@ -39,7 +40,7 @@ import { scheduleUserPermissionsSync, UserPermissionsInfoQuery } from './backend
import styles from './UserSettingsPermissionsPage.module.scss'
interface Props extends TelemetryProps {
interface Props extends TelemetryProps, TelemetryV2Props {
user: { id: string; username: string }
}
@ -49,6 +50,7 @@ interface Props extends TelemetryProps {
export const UserSettingsPermissionsPage: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
user,
telemetryService,
telemetryRecorder,
}) => {
useEffect(() => eventLogger.logViewEvent('UserSettingsPermissions'), [])
@ -142,7 +144,12 @@ export const UserSettingsPermissionsPage: React.FunctionComponent<React.PropsWit
className="my-3 pt-3"
/>
<Container className="mb-3">
<PermissionsSyncJobsTable telemetryService={telemetryService} minimal={true} userID={user.id} />
<PermissionsSyncJobsTable
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
minimal={true}
userID={user.id}
/>
</Container>
<PageHeader
headingElement="h2"

View File

@ -16,14 +16,23 @@ import {
type RenderModeSpec,
type UIRangeSpec,
} from '@sourcegraph/shared/src/util/url'
import { CallbackTelemetryProcessor } from '@sourcegraph/telemetry'
import { getWebGraphQLClient, requestGraphQL } from '../backend/graphql'
import type { TelemetryRecorderProvider } from '../telemetry'
import { eventLogger } from '../tracking/eventLogger'
/**
* Creates the {@link PlatformContext} for the web app.
*/
export function createPlatformContext(): PlatformContext {
export function createPlatformContext(props: {
/**
* The {@link TelemetryRecorderProvider} for the platform. Callers should
* make sure to configure desired buffering and add the teardown of the
* provider to a subscription or similar.
*/
telemetryRecorderProvider: TelemetryRecorderProvider
}): PlatformContext {
const settingsQueryWatcherPromise = watchViewerSettingsQuery()
const context: PlatformContext = {
@ -78,6 +87,15 @@ export function createPlatformContext(): PlatformContext {
sourcegraphURL: window.context.externalURL,
clientApplication: 'sourcegraph',
telemetryService: eventLogger,
telemetryRecorder: props.telemetryRecorderProvider.getRecorder(
window.context.debug
? [
new CallbackTelemetryProcessor(event =>
logger.info(`telemetry: ${event.feature}/${event.action}`, { event })
),
]
: undefined
),
}
return context

View File

@ -476,7 +476,13 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
path={repoNameAndRevision + repoSettingsAreaPath}
errorElement={<RouteError />}
// Always render the `RepoSettingsArea` even for empty repo to allow side-admins access it.
element={<RepoSettingsArea {...repoRevisionContainerContext} repoName={repoName} />}
element={
<RepoSettingsArea
{...repoRevisionContainerContext}
repoName={repoName}
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
}
/>
)}
<Route

View File

@ -8,6 +8,7 @@ import { of } from 'rxjs'
import { catchError } from 'rxjs/operators'
import { asError, type ErrorLike, isErrorLike } from '@sourcegraph/common'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useObservable, ErrorMessage } from '@sourcegraph/wildcard'
@ -22,13 +23,13 @@ import { RepoSettingsSidebar, type RepoSettingsSideBarGroups } from './RepoSetti
import styles from './RepoSettingsArea.module.scss'
export interface RepoSettingsAreaRouteContext extends TelemetryProps {
export interface RepoSettingsAreaRouteContext extends TelemetryProps, TelemetryV2Props {
repo: SettingsAreaRepositoryFields
}
export interface RepoSettingsAreaRoute extends RouteV6Descriptor<RepoSettingsAreaRouteContext> {}
interface Props extends BreadcrumbSetters, TelemetryProps {
interface Props extends BreadcrumbSetters, TelemetryProps, TelemetryV2Props {
repoSettingsAreaRoutes: readonly RepoSettingsAreaRoute[]
repoSettingsSidebarGroups: RepoSettingsSideBarGroups
repoName: string
@ -82,6 +83,7 @@ export const RepoSettingsArea: React.FunctionComponent<React.PropsWithChildren<P
const context: RepoSettingsAreaRouteContext = {
repo: repoOrError,
telemetryService: props.telemetryService,
telemetryRecorder: props.telemetryRecorder,
}
return (

View File

@ -3,6 +3,7 @@ import { cleanup, screen } from '@testing-library/react'
import { EMPTY, NEVER } from 'rxjs'
import sinon from 'sinon'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
@ -55,6 +56,7 @@ describe('TreePage', () => {
urlToFile: () => '',
sourcegraphURL: 'https://sourcegraph.com',
clientApplication: 'sourcegraph',
telemetryRecorder: noOpTelemetryRecorder,
},
telemetryService: NOOP_TELEMETRY_SERVICE,
codeIntelligenceEnabled: false,

View File

@ -11,6 +11,7 @@ import {
PermissionsSyncJobReasonGroup,
PermissionsSyncJobState,
} from '@sourcegraph/shared/src/graphql-operations'
import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry'
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
@ -89,7 +90,10 @@ export const SixSyncJobsFound: Story = () => (
])
}
>
<PermissionsSyncJobsTable telemetryService={NOOP_TELEMETRY_SERVICE} />
<PermissionsSyncJobsTable
telemetryService={NOOP_TELEMETRY_SERVICE}
telemetryRecorder={noOpTelemetryRecorder}
/>
</MockedTestProvider>
)}
</WebStory>

View File

@ -11,6 +11,7 @@ import { animated, useSpring } from 'react-spring'
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
import { useMutation } from '@sourcegraph/http-client'
import { convertREMToPX } from '@sourcegraph/shared/src/components/utils/size'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import {
Alert,
@ -95,7 +96,7 @@ const DEFAULT_FILTERS = {
}
export const PERMISSIONS_SYNC_JOBS_POLL_INTERVAL = 2000
interface Props extends TelemetryProps {
interface Props extends TelemetryProps, TelemetryV2Props {
minimal?: boolean
userID?: string
repoID?: string
@ -103,6 +104,7 @@ interface Props extends TelemetryProps {
export const PermissionsSyncJobsTable: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
telemetryService,
telemetryRecorder,
minimal = false,
userID,
repoID,
@ -189,6 +191,9 @@ export const PermissionsSyncJobsTable: React.FunctionComponent<React.PropsWithCh
const handleTriggerPermsSync = useCallback(
([job]: PermissionsSyncJob[]) => {
if (job.subject.__typename === 'Repository') {
telemetryRecorder.recordEvent('permissions-center.repository.sync', 'trigger', {
privateMetadata: { repo: job.subject.id },
})
triggerRepoSync({
variables: { repo: job.subject.id },
onCompleted: () =>
@ -199,6 +204,9 @@ export const PermissionsSyncJobsTable: React.FunctionComponent<React.PropsWithCh
noop
)
} else {
telemetryRecorder.recordEvent('permissions-center.user.sync', 'trigger', {
privateMetadata: { user: job.subject.id },
})
triggerUserSync({
variables: { user: job.subject.id },
onCompleted: () => toggleNotification({ text: 'User permissions sync successfully scheduled' }),
@ -209,7 +217,7 @@ export const PermissionsSyncJobsTable: React.FunctionComponent<React.PropsWithCh
)
}
},
[triggerUserSync, triggerRepoSync, onError, toggleNotification]
[triggerUserSync, triggerRepoSync, onError, toggleNotification, telemetryRecorder]
)
const handleCancelSyncJob = useCallback(
@ -223,16 +231,36 @@ export const PermissionsSyncJobsTable: React.FunctionComponent<React.PropsWithCh
// noop here is used because an error is handled in `onError` option of `useMutation` above.
noop
)
if (syncJob.subject.__typename === 'Repository') {
telemetryRecorder.recordEvent('permissions-center.repository.sync', 'cancel', {
privateMetadata: { repo: syncJob.subject.id },
})
} else {
telemetryRecorder.recordEvent('permissions-center.user.sync', 'cancel', {
privateMetadata: { user: syncJob.subject.id },
})
}
},
[cancelSyncJob, onError, toggleNotification]
[cancelSyncJob, onError, toggleNotification, telemetryRecorder]
)
const handleViewJobDetails = useCallback(
([syncJob]: PermissionsSyncJob[]) => {
setShowModal(true)
setSelectedJob(syncJob)
if (syncJob.subject.__typename === 'Repository') {
telemetryRecorder.recordEvent('permissions-center.repository.sync', 'view', {
privateMetadata: { repo: syncJob.subject.id },
})
} else {
telemetryRecorder.recordEvent('permissions-center.user.sync', 'view', {
privateMetadata: { user: syncJob.subject.id },
})
}
},
[setShowModal, setSelectedJob]
[setShowModal, setSelectedJob, telemetryRecorder]
)
if (minimal) {

View File

@ -252,7 +252,9 @@ export const otherSiteAdminRoutes: readonly SiteAdminAreaRoute[] = [
},
{
path: '/permissions-syncs',
render: props => <PermissionsSyncJobsTable {...props} />,
render: props => (
<PermissionsSyncJobsTable {...props} telemetryRecorder={props.platformContext.telemetryRecorder} />
),
},
{
path: '/gitservers',

View File

@ -0,0 +1,28 @@
import { type ApolloClient, gql } from '@apollo/client'
import type { TelemetryEventInput, TelemetryExporter } from '@sourcegraph/telemetry'
import type { ExportTelemetryEventsResult } from '../graphql-operations'
/**
* ApolloTelemetryExporter exports events via the new Sourcegraph telemetry
* framework: https://docs.sourcegraph.com/dev/background-information/telemetry
*/
export class ApolloTelemetryExporter implements TelemetryExporter {
constructor(private client: ApolloClient<object>) {}
public async exportEvents(events: TelemetryEventInput[]): Promise<void> {
await this.client.mutate<ExportTelemetryEventsResult>({
mutation: gql`
mutation ExportTelemetryEvents($events: [TelemetryEventInput!]!) {
telemetry {
recordEvents(events: $events) {
alwaysNil
}
}
}
`,
variables: { events },
})
}
}

View File

@ -0,0 +1,83 @@
import type { ApolloClient } from '@apollo/client'
import type { BillingCategory, BillingProduct } from '@sourcegraph/shared/src/telemetry'
import {
TelemetryRecorderProvider as BaseTelemetryRecorderProvider,
MarketingTrackingTelemetryProcessor,
type MarketingTrackingProvider,
type TelemetryEventMarketingTrackingInput,
} from '@sourcegraph/telemetry'
import { sessionTracker } from '../tracking/sessionTracker'
import { userTracker } from '../tracking/userTracker'
import { ApolloTelemetryExporter } from './apolloTelemetryExporter'
function getTelemetrySourceClient(): string {
if (window.context?.codyAppMode) {
return 'app.web'
}
if (window.context?.sourcegraphDotComMode) {
return 'dotcom.web'
}
return 'server.web'
}
/**
* TelemetryRecorderProvider is the default provider implementation for the
* Sourcegraph web app.
*/
export class TelemetryRecorderProvider extends BaseTelemetryRecorderProvider<BillingCategory, BillingProduct> {
constructor(
apolloClient: ApolloClient<object>,
options: {
/**
* Enables buffering of events for export. Only enable if there is a
* reliable unsubscribe mechanism available.
*/
enableBuffering: boolean
}
) {
super(
{
client: getTelemetrySourceClient(),
clientVersion: window.context.version,
},
new ApolloTelemetryExporter(apolloClient),
[new MarketingTrackingTelemetryProcessor(new TrackingMetadataProvider())],
{
/**
* Use buffer time of 100ms - some existing buffering uses
* 1000ms, but we use a more conservative value.
*/
bufferTimeMs: options.enableBuffering ? 100 : 0,
bufferMaxSize: 10,
errorHandler: error => {
throw new Error(error)
},
}
)
}
}
class TrackingMetadataProvider implements MarketingTrackingProvider {
private user = userTracker
private session = sessionTracker
public getMarketingTrackingMetadata(): TelemetryEventMarketingTrackingInput | null {
if (!window.context?.sourcegraphDotComMode) {
return null // don't report this data outside of dotcom
}
return {
cohortID: this.user.cohortID,
deviceSessionID: this.user.deviceSessionID,
firstSourceURL: this.session.getFirstSourceURL(),
lastSourceURL: this.session.getLastSourceURL(),
referrer: this.session.getReferrer(),
sessionFirstURL: this.session.getSessionFirstURL(),
sessionReferrer: this.session.getSessionReferrer(),
url: window.location.href,
}
}
}

View File

@ -77,6 +77,10 @@ export class EventLogger implements TelemetryService, SharedEventLogger {
private eventID = 0
private listeners: Set<(eventName: string) => void> = new Set()
/**
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
constructor() {
// EventLogger is never teared down
// eslint-disable-next-line rxjs/no-ignored-subscription
@ -105,6 +109,9 @@ export class EventLogger implements TelemetryService, SharedEventLogger {
/**
* Log a pageview.
* Page titles should be specific and human-readable in pascal case, e.g. "SearchResults" or "Blob" or "NewOrg"
*
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
public logViewEvent(pageTitle: string, eventProperties?: any, logAsActiveUser = true): void {
// call to refresh the session
@ -120,6 +127,8 @@ export class EventLogger implements TelemetryService, SharedEventLogger {
/**
* Log a pageview, following the new event naming conventions
*
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
* @param eventName should be specific and human-readable in pascal case, e.g. "SearchResults" or "Blob" or "NewOrg"
*/
public logPageView(eventName: string, eventProperties?: any, logAsActiveUser = true): void {
@ -137,6 +146,8 @@ export class EventLogger implements TelemetryService, SharedEventLogger {
* Log a user action or event.
* Event labels should be specific and follow a ${noun}${verb} structure in pascal case, e.g. "ButtonClicked" or "SignInInitiated"
*
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
* @param eventLabel the event name.
* @param eventProperties event properties. These get logged to our database, but do not get
* sent to our analytics systems. This may contain private info such as repository names or search queries.
@ -205,6 +216,10 @@ export class EventLogger implements TelemetryService, SharedEventLogger {
}
}
/**
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
export const eventLogger = new EventLogger()
/**

View File

@ -1,13 +1,25 @@
import { logEvent } from '../../user/settings/backend'
class ServerAdminWrapper {
/**
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
public trackPageView(eventAction: string, eventProperties?: any, publicArgument?: any): void {
logEvent(eventAction, eventProperties, publicArgument)
}
/**
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
public trackAction(eventAction: string, eventProperties?: any, publicArgument?: any): void {
logEvent(eventAction, eventProperties, publicArgument)
}
}
/**
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
export const serverAdmin = new ServerAdminWrapper()

View File

@ -17,6 +17,7 @@ export const userAreaRoutes: readonly UserAreaRoute[] = [
{...props}
routes={props.userSettingsAreaRoutes}
sideBarItems={props.userSettingsSideBarItems}
telemetryRecorder={props.platformContext.telemetryRecorder}
/>
),
},

View File

@ -5,6 +5,7 @@ import MapSearchIcon from 'mdi-react/MapSearchIcon'
import { Route, Routes } from 'react-router-dom'
import { gql, useQuery } from '@sourcegraph/http-client'
import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { LoadingSpinner } from '@sourcegraph/wildcard'
@ -30,7 +31,7 @@ import styles from './UserSettingsArea.module.scss'
export interface UserSettingsAreaRoute extends RouteV6Descriptor<UserSettingsAreaRouteContext> {}
export interface UserSettingsAreaProps extends UserAreaRouteContext, TelemetryProps {
export interface UserSettingsAreaProps extends UserAreaRouteContext, TelemetryProps, TelemetryV2Props {
authenticatedUser: AuthenticatedUser
sideBarItems: UserSettingsSidebarItems
routes: readonly UserSettingsAreaRoute[]

View File

@ -159,6 +159,9 @@ batchedEvents
*
* When invoked on a non-Sourcegraph.com instance, this data is stored in the
* instance's database, and not sent to Sourcegraph.com.
*
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
export function logEvent(event: string, eventProperties?: unknown, publicArgument?: unknown): void {
batchedEvents.next(createEvent(event, eventProperties, publicArgument))
@ -170,6 +173,9 @@ export function logEvent(event: string, eventProperties?: unknown, publicArgumen
* used only when low event latency is necessary (e.g., on an external link).
*
* See logEvent for additional details.
*
* @deprecated Use a TelemetryRecorder or TelemetryRecorderProvider from
* src/telemetry instead.
*/
export function logEventSynchronously(
event: string,

View File

@ -78,7 +78,7 @@ func (j *exporterJob) Handle(ctx context.Context) error {
// Check the current licensing mode.
if licensing.GetTelemetryEventsExportMode(conf.DefaultClient()) ==
licensing.TelemetryEventsExportDisabled {
logger.Debug("export is currently disabled entirely via licensing")
logger.Info("export is currently disabled entirely via licensing")
return nil
}

View File

@ -1,6 +1,5 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_buf//buf:defs.bzl", "buf_lint_test")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("//dev:proto.bzl", "write_proto_stubs_to_source")

View File

@ -9,9 +9,12 @@ import (
"golang.org/x/exp/slices"
"github.com/sourcegraph/sourcegraph/internal/conf/conftypes"
"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/internal/license"
)
var forceExportAll = env.MustGetBool("SRC_TELEMETRY_EVENTS_EXPORT_ALL", false, "Set to true to forcibly enable all events export.")
// telemetryExportEnablementCutOffDate is Oct 4, 2023 UTC, and all licenses
// created after this date will have telemetry export enabled by default.
//
@ -27,6 +30,10 @@ var telemetryEventsExportMode = atomic.NewPointer((*evaluatedTelemetryEventsExpo
// GetTelemetryEventsExportMode returns the degree of telemetry events export
// enabled. See TelemetryEventsExportMode for more details.
func GetTelemetryEventsExportMode(c conftypes.SiteConfigQuerier) TelemetryEventsExportMode {
if forceExportAll {
return TelemetryEventsExportAll
}
evaluatedMode := telemetryEventsExportMode.Load()
// Update if changed license key has changed

View File

@ -1495,6 +1495,9 @@ importers:
'@sourcegraph/http-client':
specifier: workspace:*
version: link:../http-client
'@sourcegraph/telemetry':
specifier: ^0.11.0
version: 0.11.0
'@sourcegraph/template-parser':
specifier: workspace:*
version: link:../template-parser
@ -1604,6 +1607,9 @@ importers:
'@sourcegraph/shared':
specifier: workspace:*
version: link:../shared
'@sourcegraph/telemetry':
specifier: ^0.11.0
version: 0.11.0
'@sourcegraph/wildcard':
specifier: workspace:*
version: link:../wildcard
@ -9507,6 +9513,12 @@ packages:
stylelint: 14.3.0
dev: true
/@sourcegraph/telemetry@0.11.0:
resolution: {integrity: sha512-cOlkCwX3V5lVJO/F8w7VauhMZob3PrMbE4DApTKwasTMwm4+OxtT6+afC5wJTwZwNWc31V83sNjV2nBkEtFPZg==}
dependencies:
rxjs: 7.8.1
dev: false
/@sourcegraph/tsconfig@4.0.1:
resolution: {integrity: sha512-G/xsejsR84G5dj3kHJ7svKBo9E5tWl96rUHKP94Y2UDtA7BzUhAYbieM+b9ZUpIRt66h3+MlYbG5HK4UI2zDzw==}
dev: true
@ -16893,7 +16905,7 @@ packages:
chalk: 4.1.2
date-fns: 2.29.3
lodash: 4.17.21
rxjs: 7.6.0
rxjs: 7.8.1
shell-quote: 1.8.1
spawn-command: 0.0.2-1
supports-color: 8.1.1
@ -22393,7 +22405,7 @@ packages:
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.6.0
rxjs: 7.8.1
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
@ -24767,7 +24779,7 @@ packages:
log-update: 4.0.0
p-map: 4.0.0
rfdc: 1.3.0
rxjs: 7.6.0
rxjs: 7.8.1
through: 2.3.8
wrap-ansi: 7.0.0
dev: true
@ -30303,11 +30315,10 @@ packages:
dependencies:
tslib: 2.1.0
/rxjs@7.6.0:
resolution: {integrity: sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==}
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies:
tslib: 2.1.0
dev: true
/sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}

View File

@ -133,6 +133,7 @@ env:
GRPC_INTERNAL_ERROR_LOGGING_LOG_PROTOBUF_MESSAGES_HANDLING_MAX_MESSAGE_SIZE_BYTES: "100MB"
TELEMETRY_GATEWAY_EXPORTER_EXPORT_ADDR: "http://127.0.0.1:10080"
SRC_TELEMETRY_EVENTS_EXPORT_ALL: "true"
commands:
server: