mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 13:11:49 +00:00
Add v2 telemetry infrastructure to browser extensions and native inte… (#63458)
…grations This PR adds v2t telemetry infrastructure to the Sourcegraph browser extensions and native integrations code base. ## Test plan - Tested locally using instructions at https://github.com/sourcegraph/sourcegraph/tree/main/client/browser - CI ## Changelog --------- Co-authored-by: Dan Adler <5589410+dadlerj@users.noreply.github.com>
This commit is contained in:
parent
239f42947b
commit
4c824b4aa8
@ -211,6 +211,8 @@ ts_project(
|
||||
"src/shared/repo/backend.tsx",
|
||||
"src/shared/repo/index.tsx",
|
||||
"src/shared/sentry/index.ts",
|
||||
"src/shared/telemetry/gqlTelemetryExporter.ts",
|
||||
"src/shared/telemetry/index.ts",
|
||||
"src/shared/tracking/eventLogger.tsx",
|
||||
"src/shared/util/context.tsx",
|
||||
"src/shared/util/dom.tsx",
|
||||
@ -231,6 +233,7 @@ ts_project(
|
||||
":node_modules/@sourcegraph/extension-api-types",
|
||||
":node_modules/@sourcegraph/http-client",
|
||||
":node_modules/@sourcegraph/shared",
|
||||
":node_modules/@sourcegraph/telemetry",
|
||||
":node_modules/@sourcegraph/wildcard",
|
||||
"//:node_modules/@mdi/js",
|
||||
"//:node_modules/@reach/combobox",
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"@sourcegraph/branded": "workspace:*",
|
||||
"@sourcegraph/http-client": "workspace:*",
|
||||
"@sourcegraph/client-api": "workspace:*",
|
||||
"@sourcegraph/codeintellify": "workspace:*"
|
||||
"@sourcegraph/codeintellify": "workspace:*",
|
||||
"@sourcegraph/telemetry": "^0.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import { initializeOmniboxInterface } from '../../shared/cli'
|
||||
import { browserPortToMessagePort, findMessagePorts } from '../../shared/platform/ports'
|
||||
import { createBlobURLForBundle } from '../../shared/platform/worker'
|
||||
import { initSentry } from '../../shared/sentry'
|
||||
import { ConditionalTelemetryRecorderProvider } from '../../shared/telemetry'
|
||||
import { EventLogger } from '../../shared/tracking/eventLogger'
|
||||
import { getExtensionVersion, getPlatformName, observeSourcegraphURL } from '../../shared/util/context'
|
||||
import { type BrowserActionIconState, setBrowserActionIconState } from '../browser-action-icon'
|
||||
@ -148,6 +149,9 @@ async function main(): Promise<void> {
|
||||
.log('BrowserExtensionInstalled')
|
||||
.then(() => console.log(`Triggered "BrowserExtensionInstalled" using ${sourcegraphURL}`))
|
||||
.catch(error => console.error('Error triggering "BrowserExtensionInstalled" event:', error))
|
||||
new ConditionalTelemetryRecorderProvider(of(true), requestGraphQL)
|
||||
.getRecorder()
|
||||
.recordEvent('browserExtension', 'install')
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import type { Optional } from 'utility-types'
|
||||
|
||||
import { asError, isDefined } from '@sourcegraph/common'
|
||||
import { gql, type GraphQLResult } from '@sourcegraph/http-client'
|
||||
import type { BillingCategory, BillingProduct } from '@sourcegraph/shared/src/telemetry'
|
||||
import type { TelemetryService } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { setLinkComponent, AnchorLink, useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
@ -21,6 +22,11 @@ import type { CurrentUserResult } from '../../graphql-operations'
|
||||
import { fetchSite } from '../../shared/backend/server'
|
||||
import { WildcardThemeProvider } from '../../shared/components/WildcardThemeProvider'
|
||||
import { initSentry } from '../../shared/sentry'
|
||||
import {
|
||||
type ConditionalTelemetryRecorder,
|
||||
ConditionalTelemetryRecorderProvider,
|
||||
noOpTelemetryRecorder,
|
||||
} from '../../shared/telemetry'
|
||||
import { ConditionalTelemetryService, EventLogger } from '../../shared/tracking/eventLogger'
|
||||
import { observeSourcegraphURL, getExtensionVersion, isDefaultSourcegraphUrl } from '../../shared/util/context'
|
||||
import { featureFlags } from '../../shared/util/featureFlags'
|
||||
@ -141,6 +147,31 @@ function useTelemetryService(sourcegraphUrl: string | undefined): TelemetryServi
|
||||
return telemetryService
|
||||
}
|
||||
|
||||
function useTelemetryRecorder(
|
||||
sourcegraphUrl: string | undefined
|
||||
): ConditionalTelemetryRecorder<BillingCategory, BillingProduct> {
|
||||
const telemetryRecorder = useMemo(() => {
|
||||
if (!sourcegraphUrl) {
|
||||
return noOpTelemetryRecorder
|
||||
}
|
||||
const telemetryRecorderProvider = new ConditionalTelemetryRecorderProvider(
|
||||
observingSendTelemetry,
|
||||
createRequestGraphQL(sourcegraphUrl)
|
||||
)
|
||||
return telemetryRecorderProvider.getRecorder()
|
||||
}, [sourcegraphUrl])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (telemetryRecorder !== noOpTelemetryRecorder) {
|
||||
telemetryRecorder.unsubscribe()
|
||||
}
|
||||
},
|
||||
[telemetryRecorder]
|
||||
)
|
||||
return telemetryRecorder
|
||||
}
|
||||
|
||||
const fetchCurrentUser = (
|
||||
sourcegraphURL: string
|
||||
): Observable<Pick<NonNullable<CurrentUserResult['currentUser']>, 'settingsURL' | 'siteAdmin'>> => {
|
||||
@ -173,6 +204,7 @@ const Options: React.FunctionComponent<React.PropsWithChildren<unknown>> = () =>
|
||||
const sourcegraphUrl = useObservable(observingSourcegraphUrl)
|
||||
const [previousSourcegraphUrl, setPreviousSourcegraphUrl] = useState(sourcegraphUrl)
|
||||
const telemetryService = useTelemetryService(sourcegraphUrl)
|
||||
const telemetryRecorder = useTelemetryRecorder(sourcegraphUrl)
|
||||
const previouslyUsedUrls = useObservable(observingPreviouslyUsedUrls)
|
||||
const isActivated = useObservable(observingIsActivated)
|
||||
const optionFlagsWithValues = useObservable(observingOptionFlagsWithValues)
|
||||
@ -246,9 +278,10 @@ const Options: React.FunctionComponent<React.PropsWithChildren<unknown>> = () =>
|
||||
const handleToggleActivated = useCallback(
|
||||
(isActivated: boolean): void => {
|
||||
telemetryService.log(isActivated ? 'BrowserExtensionEnabled' : 'BrowserExtensionDisabled')
|
||||
telemetryRecorder.recordEvent('browserExtension', isActivated ? 'enabled' : 'disabled')
|
||||
storage.sync.set({ disableExtension: !isActivated }).catch(console.error)
|
||||
},
|
||||
[telemetryService]
|
||||
[telemetryService, telemetryRecorder]
|
||||
)
|
||||
|
||||
const handleRemovePreviousSourcegraphUrl = useCallback(
|
||||
|
||||
@ -97,6 +97,7 @@ import { WildcardThemeProvider } from '../../components/WildcardThemeProvider'
|
||||
import { isExtension, isInPage } from '../../context'
|
||||
import type { SourcegraphIntegrationURLs, BrowserPlatformContext } from '../../platform/context'
|
||||
import { resolveRevision, retryWhenCloneInProgressError, resolvePrivateRepo } from '../../repo/backend'
|
||||
import { ConditionalTelemetryRecorderProvider } from '../../telemetry'
|
||||
import { ConditionalTelemetryService, EventLogger } from '../../tracking/eventLogger'
|
||||
import { DEFAULT_SOURCEGRAPH_URL, getPlatformName, isDefaultSourcegraphUrl } from '../../util/context'
|
||||
import { type MutationRecordLike, querySelectorOrSelf } from '../../util/dom'
|
||||
@ -1364,6 +1365,11 @@ export function injectCodeIntelligenceToCodeHost(
|
||||
const telemetryService = new ConditionalTelemetryService(innerTelemetryService, isTelemetryEnabled)
|
||||
subscriptions.add(telemetryService)
|
||||
|
||||
const telemetryRecorderProvider = new ConditionalTelemetryRecorderProvider(isTelemetryEnabled, requestGraphQL)
|
||||
const telemetryRecorder = telemetryRecorderProvider.getRecorder()
|
||||
subscriptions.add(telemetryRecorder)
|
||||
platformContext.telemetryRecorder = telemetryRecorder
|
||||
|
||||
let codeHostSubscription: Subscription
|
||||
// In the browser extension, observe whether the `disableExtension` storage flag is set.
|
||||
// In the native integration, this flag does not exist.
|
||||
@ -1402,7 +1408,7 @@ export function injectCodeIntelligenceToCodeHost(
|
||||
extensionsController,
|
||||
platformContext,
|
||||
telemetryService,
|
||||
telemetryRecorder: platformContext.telemetryRecorder,
|
||||
telemetryRecorder,
|
||||
render: renderWithThemeProvider as Renderer,
|
||||
minimalUI,
|
||||
hideActions,
|
||||
|
||||
@ -6,11 +6,11 @@ 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'
|
||||
import type { CodeHost } from '../code-hosts/shared/codeHost'
|
||||
import { noOpTelemetryRecorder } from '../telemetry'
|
||||
|
||||
import { createExtensionHost } from './extensionHost'
|
||||
import { getInlineExtensions } from './inlineExtensionsService'
|
||||
@ -130,11 +130,9 @@ export function createPlatformContext(
|
||||
clientApplication: 'other',
|
||||
getStaticExtensions: () => getInlineExtensions(assetsURL),
|
||||
/**
|
||||
* @todo Not yet implemented!
|
||||
* This will be replaced by a real telemetry recorder in codeHost.tsx.
|
||||
*/
|
||||
telemetryRecorder: new NoOpTelemetryRecorderProvider({
|
||||
errorOnRecord: true, // should not be used at all
|
||||
}).getRecorder(),
|
||||
telemetryRecorder: noOpTelemetryRecorder,
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
40
client/browser/src/shared/telemetry/gqlTelemetryExporter.ts
Normal file
40
client/browser/src/shared/telemetry/gqlTelemetryExporter.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { gql } from '@sourcegraph/http-client'
|
||||
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
import type { TelemetryEventInput, TelemetryExporter } from '@sourcegraph/telemetry'
|
||||
|
||||
// todo(dan) update with new recordeventresult type?
|
||||
import type { logEventResult } from '../../graphql-operations'
|
||||
|
||||
/**
|
||||
* GraphQLTelemetryExporter exports events via the new Sourcegraph telemetry
|
||||
* framework: https://sourcegraph.com/docs/dev/background-information/telemetry
|
||||
*/
|
||||
export class GraphQLTelemetryExporter implements TelemetryExporter {
|
||||
constructor(private requestGraphQL: PlatformContext['requestGraphQL']) {}
|
||||
|
||||
public exportEvents(events: TelemetryEventInput[]): Promise<void> {
|
||||
const req = this.requestGraphQL<logEventResult>({
|
||||
request: gql`
|
||||
mutation ExportTelemetryEventsFromBrowserExtension($events: [TelemetryEventInput!]!) {
|
||||
telemetry {
|
||||
recordEvents(events: $events) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { events },
|
||||
mightContainPrivateInfo: false,
|
||||
})
|
||||
// eslint-disable-next-line rxjs/no-ignored-subscription
|
||||
req.subscribe({
|
||||
error: _ => {
|
||||
// Swallow errors. If a Sourcegraph instance isn't upgraded, this request may fail.
|
||||
// However, end users shouldn't experience this failure, as their admin is
|
||||
// responsible for updating the instance, and has been (or will be) notified
|
||||
// that an upgrade is available via site-admin messaging.
|
||||
},
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
104
client/browser/src/shared/telemetry/index.ts
Normal file
104
client/browser/src/shared/telemetry/index.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { type Observable, Subscription } from 'rxjs'
|
||||
|
||||
import type { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
import type { BillingCategory, BillingProduct } from '@sourcegraph/shared/src/telemetry'
|
||||
import {
|
||||
type TelemetryRecorder as BaseTelemetryRecorder,
|
||||
TelemetryRecorderProvider as BaseTelemetryRecorderProvider,
|
||||
type KnownKeys,
|
||||
type KnownString,
|
||||
NoOpTelemetryExporter,
|
||||
type TelemetryEventParameters,
|
||||
} from '@sourcegraph/telemetry'
|
||||
|
||||
import { getTelemetryClientName } from '../util/context'
|
||||
|
||||
import { GraphQLTelemetryExporter } from './gqlTelemetryExporter'
|
||||
|
||||
/**
|
||||
* TelemetryRecorderProvider is the default provider implementation for the
|
||||
* Sourcegraph web app.
|
||||
*/
|
||||
export class ConditionalTelemetryRecorderProvider extends BaseTelemetryRecorderProvider<
|
||||
BillingCategory,
|
||||
BillingProduct
|
||||
> {
|
||||
constructor(private telemetryEnabled: Observable<boolean>, requestGraphQL: PlatformContext['requestGraphQL']) {
|
||||
super(
|
||||
{
|
||||
client: getTelemetryClientName(),
|
||||
},
|
||||
new GraphQLTelemetryExporter(requestGraphQL),
|
||||
[],
|
||||
{
|
||||
/**
|
||||
* Disable buffering for now
|
||||
*/
|
||||
bufferTimeMs: 0,
|
||||
bufferMaxSize: 1,
|
||||
errorHandler: error => {
|
||||
throw new Error(error)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public getRecorder(): ConditionalTelemetryRecorder<BillingCategory, BillingProduct> {
|
||||
return new ConditionalTelemetryRecorder(this.telemetryEnabled, super.getRecorder())
|
||||
}
|
||||
}
|
||||
|
||||
export class ConditionalTelemetryRecorder<BillingCategory extends string, BillingProduct extends string>
|
||||
implements BaseTelemetryRecorder<BillingCategory, BillingProduct>
|
||||
{
|
||||
/** The enabled state set by an observable, provided upon instantiation */
|
||||
private isEnabled = false
|
||||
/** Log events are passed on to the inner TelemetryService */
|
||||
private subscription = new Subscription()
|
||||
|
||||
constructor(
|
||||
telemetryEnabled: Observable<boolean>,
|
||||
private innerRecorder: BaseTelemetryRecorder<BillingCategory, BillingProduct>
|
||||
) {
|
||||
this.subscription.add(
|
||||
telemetryEnabled.subscribe(enabled => {
|
||||
this.isEnabled = enabled
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public recordEvent<Feature extends string, Action extends string, MetadataKey extends string>(
|
||||
feature: KnownString<Feature>,
|
||||
action: KnownString<Action>,
|
||||
parameters?:
|
||||
| TelemetryEventParameters<
|
||||
KnownKeys<MetadataKey, { [key in MetadataKey]: number }>,
|
||||
BillingCategory,
|
||||
BillingProduct
|
||||
>
|
||||
| undefined
|
||||
): void {
|
||||
setTimeout(() => {
|
||||
if (this.isEnabled) {
|
||||
this.innerRecorder.recordEvent(feature, action, parameters)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
this.isEnabled = false
|
||||
this.subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpTelemetryRecorderProvider extends BaseTelemetryRecorderProvider<BillingCategory, BillingProduct> {
|
||||
constructor() {
|
||||
super({ client: '' }, new NoOpTelemetryExporter(), [])
|
||||
}
|
||||
}
|
||||
|
||||
export const noOptelemetryRecorderProvider = new NoOpTelemetryRecorderProvider()
|
||||
export const noOpTelemetryRecorder = noOptelemetryRecorderProvider.getRecorder() as ConditionalTelemetryRecorder<
|
||||
BillingCategory,
|
||||
BillingProduct
|
||||
>
|
||||
@ -50,6 +50,22 @@ export function getPlatformName(): PlatformName {
|
||||
return isFirefox() ? 'firefox-extension' : 'chrome-extension'
|
||||
}
|
||||
|
||||
export function getTelemetryClientName(): string {
|
||||
if (window.SOURCEGRAPH_PHABRICATOR_EXTENSION || window.SOURCEGRAPH_INTEGRATION === 'phabricator-integration') {
|
||||
return 'phabricator.integration'
|
||||
}
|
||||
if (window.SOURCEGRAPH_INTEGRATION === 'bitbucket-integration') {
|
||||
return 'bitbucket.integration'
|
||||
}
|
||||
if (window.SOURCEGRAPH_INTEGRATION === 'gitlab-integration') {
|
||||
return 'gitlab.integration'
|
||||
}
|
||||
if (isSafari()) {
|
||||
return 'safari.browserExtension'
|
||||
}
|
||||
return isFirefox() ? 'firefox.browserExtension' : 'chrome.browserExtension'
|
||||
}
|
||||
|
||||
export function getExtensionVersion(): string {
|
||||
if (globalThis.browser) {
|
||||
const manifest = browser.runtime.getManifest()
|
||||
|
||||
@ -1149,6 +1149,9 @@ importers:
|
||||
'@sourcegraph/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@sourcegraph/telemetry':
|
||||
specifier: ^0.16.0
|
||||
version: 0.16.0
|
||||
'@sourcegraph/wildcard':
|
||||
specifier: workspace:*
|
||||
version: link:../wildcard
|
||||
|
||||
Loading…
Reference in New Issue
Block a user