mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:12:02 +00:00
update version consts in updatecheck handler, temp solutions see: https://linear.app/sourcegraph/issue/REL-145/release-tie-release-consts-to-release-registry ## Test plan No test <!-- REQUIRED; info at https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles --> ## Changelog <!-- OPTIONAL; info at https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c -->
734 lines
33 KiB
Go
734 lines
33 KiB
Go
package updatecheck
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/coreos/go-semver/semver"
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
"github.com/sourcegraph/log"
|
|
|
|
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot"
|
|
"github.com/sourcegraph/sourcegraph/cmd/frontend/hubspot/hubspotutil"
|
|
"github.com/sourcegraph/sourcegraph/internal/conf/deploy"
|
|
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
|
|
"github.com/sourcegraph/sourcegraph/internal/pubsub"
|
|
"github.com/sourcegraph/sourcegraph/internal/types"
|
|
"github.com/sourcegraph/sourcegraph/lib/errors"
|
|
)
|
|
|
|
var (
|
|
// latestReleaseDockerServerImageBuild is only used by sourcegraph.com to tell existing
|
|
// non-cluster, non-docker-compose, and non-pure-docker installations what the latest
|
|
// version is. The version here _must_ be available at https://hub.docker.com/r/sourcegraph/server/tags/
|
|
// before landing in master.
|
|
latestReleaseDockerServerImageBuild = newPingResponse("5.5.2463")
|
|
|
|
// latestReleaseKubernetesBuild is only used by sourcegraph.com to tell existing Sourcegraph
|
|
// cluster deployments what the latest version is. The version here _must_ be available in
|
|
// a tag at https://github.com/sourcegraph/deploy-sourcegraph before landing in master.
|
|
latestReleaseKubernetesBuild = newPingResponse("5.5.2463")
|
|
|
|
// latestReleaseDockerComposeOrPureDocker is only used by sourcegraph.com to tell existing Sourcegraph
|
|
// Docker Compose or Pure Docker deployments what the latest version is. The version here _must_ be
|
|
// available in a tag at https://github.com/sourcegraph/deploy-sourcegraph-docker before landing in master.
|
|
latestReleaseDockerComposeOrPureDocker = newPingResponse("5.5.2463")
|
|
)
|
|
|
|
func getLatestRelease(deployType string) pingResponse {
|
|
switch {
|
|
case deploy.IsDeployTypeKubernetes(deployType):
|
|
return latestReleaseKubernetesBuild
|
|
case deploy.IsDeployTypeDockerCompose(deployType), deploy.IsDeployTypePureDocker(deployType):
|
|
return latestReleaseDockerComposeOrPureDocker
|
|
default:
|
|
return latestReleaseDockerServerImageBuild
|
|
}
|
|
}
|
|
|
|
// ForwardHandler returns a handler that forwards the request to
|
|
// https://pings.sourcegraph.com.
|
|
func ForwardHandler() (http.HandlerFunc, error) {
|
|
remote, err := url.Parse(defaultUpdateCheckURL)
|
|
if err != nil {
|
|
return nil, errors.Errorf("parse default update check URL: %v", err)
|
|
}
|
|
|
|
// If remote has a path, the proxy server will always append an unnecessary "/" to the path.
|
|
remotePath := remote.Path
|
|
remote.Path = ""
|
|
proxy := httputil.NewSingleHostReverseProxy(remote)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
r.Host = remote.Host
|
|
r.URL.Path = remotePath
|
|
proxy.ServeHTTP(w, r)
|
|
}, nil
|
|
}
|
|
|
|
type Meter struct {
|
|
RequestCounter metric.Int64Counter
|
|
RequestHasUpdateCounter metric.Int64Counter
|
|
ErrorCounter metric.Int64Counter
|
|
}
|
|
|
|
// Handle handles the ping requests and responds with information about software
|
|
// updates for Sourcegraph.
|
|
func Handle(logger log.Logger, pubsubClient pubsub.TopicPublisher, meter *Meter, w http.ResponseWriter, r *http.Request) {
|
|
meter.RequestCounter.Add(r.Context(), 1)
|
|
|
|
pr, err := readPingRequest(r)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if pr.ClientSiteID == "" {
|
|
logger.Error("no site ID specified")
|
|
http.Error(w, "no site ID specified", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if pr.ClientVersionString == "" {
|
|
logger.Error("no version specified")
|
|
http.Error(w, "no version specified", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if pr.ClientVersionString == "dev" {
|
|
// No updates for dev servers.
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
pingResponse := getLatestRelease(pr.DeployType)
|
|
hasUpdate, err := canUpdate(pr.ClientVersionString, pingResponse)
|
|
|
|
// Always log, even on malformed version strings
|
|
logPing(logger, pubsubClient, meter, r, pr, hasUpdate)
|
|
|
|
if err != nil {
|
|
http.Error(w, pr.ClientVersionString+" is a bad version string: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
body, err := json.Marshal(pingResponse)
|
|
if err != nil {
|
|
logger.Error("error preparing update check response", log.Error(err))
|
|
http.Error(w, "", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if !hasUpdate {
|
|
// No newer version.
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
w.Header().Set("content-type", "application/json; charset=utf-8")
|
|
meter.RequestHasUpdateCounter.Add(r.Context(), 1)
|
|
_, _ = w.Write(body)
|
|
}
|
|
|
|
// canUpdate returns true if the latestReleaseBuild is newer than the clientVersionString.
|
|
func canUpdate(clientVersionString string, latestReleaseBuild pingResponse) (bool, error) {
|
|
// Check for a date in the version string to handle developer builds that don't have a semver.
|
|
// If there is an error parsing a date out of the version string, then we ignore the error
|
|
// and parse it as a semver.
|
|
if hasDateUpdate, err := canUpdateDate(clientVersionString); err == nil {
|
|
return hasDateUpdate, nil
|
|
}
|
|
|
|
// Released builds will have a semantic version that we can compare.
|
|
return canUpdateVersion(clientVersionString, latestReleaseBuild)
|
|
}
|
|
|
|
// canUpdateVersion returns true if the latest released build is newer than
|
|
// the clientVersionString. It returns an error if clientVersionString is not a semver.
|
|
func canUpdateVersion(clientVersionString string, latestReleaseBuild pingResponse) (bool, error) {
|
|
clientVersionString = strings.TrimPrefix(clientVersionString, "v")
|
|
clientVersion, err := semver.NewVersion(clientVersionString)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return clientVersion.LessThan(latestReleaseBuild.Version), nil
|
|
}
|
|
|
|
var (
|
|
dateRegex = lazyregexp.New("_([0-9]{4}-[0-9]{2}-[0-9]{2})_")
|
|
timeNow = time.Now
|
|
)
|
|
|
|
// canUpdateDate returns true if clientVersionString contains a date
|
|
// more than 40 days in the past. It returns an error if there is no
|
|
// parsable date in clientVersionString
|
|
func canUpdateDate(clientVersionString string) (bool, error) {
|
|
match := dateRegex.FindStringSubmatch(clientVersionString)
|
|
if len(match) != 2 {
|
|
return false, errors.Errorf("no date in version string %q", clientVersionString)
|
|
}
|
|
|
|
t, err := time.ParseInLocation("2006-01-02", match[1], time.UTC)
|
|
if err != nil {
|
|
// This shouldn't ever happen if the above code is correct.
|
|
return false, err
|
|
}
|
|
|
|
// Assume that we release a new version at least every 40 days.
|
|
return timeNow().After(t.Add(40 * 24 * time.Hour)), nil
|
|
}
|
|
|
|
// pingRequest is the payload of the update check request. These values either
|
|
// supplied via query string or by a JSON body (when the request method is POST).
|
|
// We need to maintain backwards compatibility with the GET-only update checks
|
|
// while expanding the payload size for newer instance versions (via HTTP body).
|
|
type pingRequest struct {
|
|
ClientSiteID string `json:"site"`
|
|
LicenseKey string `json:",omitempty"`
|
|
ExternalURL string `json:"externalURL,omitempty"`
|
|
DeployType string `json:"deployType"`
|
|
Os string `json:"os,omitempty"` // Only used in Cody App
|
|
ClientVersionString string `json:"version"`
|
|
DependencyVersions json.RawMessage `json:"dependencyVersions,omitempty"`
|
|
AuthProviders []string `json:"auth,omitempty"`
|
|
ExternalServices []string `json:"extsvcs,omitempty"`
|
|
BuiltinSignupAllowed bool `json:"signup,omitempty"`
|
|
AccessRequestEnabled bool `json:"accessRequestEnabled,omitempty"`
|
|
HasExtURL bool `json:"hasExtURL,omitempty"`
|
|
UniqueUsers int32 `json:"u,omitempty"`
|
|
Activity json.RawMessage `json:"act,omitempty"`
|
|
BatchChangesUsage json.RawMessage `json:"batchChangesUsage,omitempty"`
|
|
// AutomationUsage (campaigns) is deprecated, but here so we can receive pings from older instances
|
|
AutomationUsage json.RawMessage `json:"automationUsage,omitempty"`
|
|
GrowthStatistics json.RawMessage `json:"growthStatistics,omitempty"`
|
|
SavedSearches json.RawMessage `json:"savedSearches,omitempty"`
|
|
Prompts json.RawMessage `json:"prompts,omitempty"`
|
|
HomepagePanels json.RawMessage `json:"homepagePanels,omitempty"`
|
|
SearchOnboarding json.RawMessage `json:"searchOnboarding,omitempty"`
|
|
Repositories json.RawMessage `json:"repositories,omitempty"`
|
|
RepositorySizeHistogram json.RawMessage `json:"repository_size_histogram,omitempty"`
|
|
RetentionStatistics json.RawMessage `json:"retentionStatistics,omitempty"`
|
|
CodeIntelUsage json.RawMessage `json:"codeIntelUsage,omitempty"`
|
|
NewCodeIntelUsage json.RawMessage `json:"newCodeIntelUsage,omitempty"`
|
|
SearchUsage json.RawMessage `json:"searchUsage,omitempty"`
|
|
ExtensionsUsage json.RawMessage `json:"extensionsUsage,omitempty"`
|
|
CodeInsightsUsage json.RawMessage `json:"codeInsightsUsage,omitempty"`
|
|
SearchJobsUsage json.RawMessage `json:"searchJobsUsage,omitempty"`
|
|
CodeInsightsCriticalTelemetry json.RawMessage `json:"codeInsightsCriticalTelemetry,omitempty"`
|
|
CodeMonitoringUsage json.RawMessage `json:"codeMonitoringUsage,omitempty"`
|
|
NotebooksUsage json.RawMessage `json:"notebooksUsage,omitempty"`
|
|
CodeHostVersions json.RawMessage `json:"codeHostVersions,omitempty"`
|
|
CodeHostIntegrationUsage json.RawMessage `json:"codeHostIntegrationUsage,omitempty"`
|
|
IDEExtensionsUsage json.RawMessage `json:"ideExtensionsUsage,omitempty"`
|
|
MigratedExtensionsUsage json.RawMessage `json:"migratedExtensionsUsage,omitempty"`
|
|
OwnUsage json.RawMessage `json:"ownUsage,omitempty"`
|
|
InitialAdminEmail string `json:"initAdmin,omitempty"`
|
|
TosAccepted bool `json:"tosAccepted,omitempty"`
|
|
TotalUsers int32 `json:"totalUsers,omitempty"`
|
|
TotalOrgs int32 `json:"totalOrgs,omitempty"`
|
|
TotalRepos int32 `json:"totalRepos,omitempty"` // Only used in Cody App
|
|
HasRepos bool `json:"repos,omitempty"`
|
|
EverSearched bool `json:"searched,omitempty"`
|
|
EverFindRefs bool `json:"refs,omitempty"`
|
|
ActiveToday bool `json:"activeToday,omitempty"` // Only used in Cody App
|
|
HasCodyEnabled bool `json:"hasCodyEnabled,omitempty"`
|
|
// CodyUsage is deprecated, but here so we can receive pings from older instances
|
|
CodyUsage json.RawMessage `json:"codyUsage,omitempty"`
|
|
CodyUsage2 json.RawMessage `json:"codyUsage2,omitempty"`
|
|
CodyProviders json.RawMessage `json:"codyProviders,omitempty"`
|
|
RepoMetadataUsage json.RawMessage `json:"repoMetadataUsage,omitempty"`
|
|
LlmUsage json.RawMessage `json:"llmUsage,omitempty"`
|
|
CodyContextFiltersConfigured bool `json:"codyContextFiltersConfigured,omitempty"`
|
|
}
|
|
|
|
type dependencyVersions struct {
|
|
PostgresVersion string `json:"postgresVersion"`
|
|
RedisCacheVersion string `json:"redisCacheVersion"`
|
|
RedisStoreVersion string `json:"redisStoreVersion"`
|
|
}
|
|
|
|
// readPingRequest reads the ping request payload from the request. If the
|
|
// request method is GET, it will read all parameters from the query string.
|
|
// If the request method is POST, it will read the parameters via a JSON
|
|
// encoded HTTP body.
|
|
func readPingRequest(r *http.Request) (*pingRequest, error) {
|
|
if r.Method == "GET" {
|
|
return readPingRequestFromQuery(r.URL.Query())
|
|
}
|
|
|
|
return readPingRequestFromBody(r.Body)
|
|
}
|
|
|
|
func readPingRequestFromQuery(q url.Values) (*pingRequest, error) {
|
|
return &pingRequest{
|
|
ClientSiteID: q.Get("site"),
|
|
// LicenseKey was added after the switch from query strings to POST data, so it's not
|
|
// available.
|
|
DeployType: q.Get("deployType"),
|
|
ClientVersionString: q.Get("version"),
|
|
AuthProviders: strings.Split(q.Get("auth"), ","),
|
|
ExternalServices: strings.Split(q.Get("extsvcs"), ","),
|
|
BuiltinSignupAllowed: toBool(q.Get("signup")),
|
|
AccessRequestEnabled: toBool(q.Get("accessRequestEnabled")),
|
|
HasExtURL: toBool(q.Get("hasExtURL")),
|
|
UniqueUsers: toInt(q.Get("u")),
|
|
Activity: toRawMessage(q.Get("act")),
|
|
InitialAdminEmail: q.Get("initAdmin"),
|
|
TotalUsers: toInt(q.Get("totalUsers")),
|
|
HasRepos: toBool(q.Get("repos")),
|
|
EverSearched: toBool(q.Get("searched")),
|
|
EverFindRefs: toBool(q.Get("refs")),
|
|
TosAccepted: toBool(q.Get("tosAccepted")),
|
|
}, nil
|
|
}
|
|
|
|
func readPingRequestFromBody(body io.ReadCloser) (*pingRequest, error) {
|
|
defer func() { _ = body.Close() }()
|
|
contents, err := io.ReadAll(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var payload *pingRequest
|
|
if err := json.Unmarshal(contents, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func toInt(val string) int32 {
|
|
value, err := strconv.ParseInt(val, 10, 32)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return int32(value)
|
|
}
|
|
|
|
func toBool(val string) bool {
|
|
value, err := strconv.ParseBool(val)
|
|
return err == nil && value
|
|
}
|
|
|
|
func toRawMessage(val string) json.RawMessage {
|
|
if val == "" {
|
|
return nil
|
|
}
|
|
var payload json.RawMessage
|
|
_ = json.Unmarshal([]byte(val), &payload)
|
|
return payload
|
|
}
|
|
|
|
type pingPayload struct {
|
|
RemoteIP string `json:"remote_ip"`
|
|
RemoteSiteVersion string `json:"remote_site_version"`
|
|
RemoteSiteID string `json:"remote_site_id"`
|
|
LicenseKey string `json:"license_key"`
|
|
ExternalURL string `json:"external_url"`
|
|
HasUpdate string `json:"has_update"`
|
|
UniqueUsersToday string `json:"unique_users_today"`
|
|
SiteActivity json.RawMessage `json:"site_activity"`
|
|
BatchChangesUsage json.RawMessage `json:"batch_changes_usage"`
|
|
CodeIntelUsage json.RawMessage `json:"code_intel_usage"`
|
|
NewCodeIntelUsage json.RawMessage `json:"new_code_intel_usage"`
|
|
SearchUsage json.RawMessage `json:"search_usage"`
|
|
GrowthStatistics json.RawMessage `json:"growth_statistics"`
|
|
SavedSearches json.RawMessage `json:"saved_searches"`
|
|
HomepagePanels json.RawMessage `json:"homepage_panels"`
|
|
RetentionStatistics json.RawMessage `json:"retention_statistics"`
|
|
Repositories json.RawMessage `json:"repositories"`
|
|
RepositorySizeHistogram json.RawMessage `json:"repository_size_histogram"`
|
|
SearchOnboarding json.RawMessage `json:"search_onboarding"`
|
|
DependencyVersions json.RawMessage `json:"dependency_versions"`
|
|
ExtensionsUsage json.RawMessage `json:"extensions_usage"`
|
|
CodeInsightsUsage json.RawMessage `json:"code_insights_usage"`
|
|
SearchJobsUsage json.RawMessage `json:"search_jobs_usage"`
|
|
CodeInsightsCriticalTelemetry json.RawMessage `json:"code_insights_critical_telemetry"`
|
|
CodeMonitoringUsage json.RawMessage `json:"code_monitoring_usage"`
|
|
NotebooksUsage json.RawMessage `json:"notebooks_usage"`
|
|
CodeHostVersions json.RawMessage `json:"code_host_versions"`
|
|
CodeHostIntegrationUsage json.RawMessage `json:"code_host_integration_usage"`
|
|
IDEExtensionsUsage json.RawMessage `json:"ide_extensions_usage"`
|
|
MigratedExtensionsUsage json.RawMessage `json:"migrated_extensions_usage"`
|
|
OwnUsage json.RawMessage `json:"own_usage"`
|
|
InstallerEmail string `json:"installer_email"`
|
|
AuthProviders string `json:"auth_providers"`
|
|
ExtServices string `json:"ext_services"`
|
|
BuiltinSignupAllowed string `json:"builtin_signup_allowed"`
|
|
AccessRequestEnabled string `json:"access_request_enabled"`
|
|
DeployType string `json:"deploy_type"`
|
|
TotalUserAccounts string `json:"total_user_accounts"`
|
|
TotalRepos string `json:"total_repos"`
|
|
HasExternalURL string `json:"has_external_url"`
|
|
HasRepos string `json:"has_repos"`
|
|
EverSearched string `json:"ever_searched"`
|
|
EverFindRefs string `json:"ever_find_refs"`
|
|
Os string `json:"os"`
|
|
ActiveToday string `json:"active_today"`
|
|
Timestamp string `json:"timestamp"`
|
|
HasCodyEnabled string `json:"has_cody_enabled"`
|
|
CodyUsage2 json.RawMessage `json:"cody_usage_2"`
|
|
CodyProviders json.RawMessage `json:"cody_providers"`
|
|
RepoMetadataUsage json.RawMessage `json:"repo_metadata_usage"`
|
|
LlmUsage json.RawMessage `json:"llm_usage"`
|
|
CodyContextFiltersConfigured string `json:"cody_context_filters_configured"`
|
|
}
|
|
|
|
func logPing(logger log.Logger, pubsubClient pubsub.TopicPublisher, meter *Meter, r *http.Request, pr *pingRequest, hasUpdate bool) {
|
|
logger = logger.Scoped("logPing")
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
logger.Warn("panic", log.String("recover", fmt.Sprintf("%+v", err)))
|
|
meter.ErrorCounter.Add(r.Context(), 1)
|
|
}
|
|
}()
|
|
|
|
// Sync the initial administrator email in HubSpot.
|
|
if strings.Contains(pr.InitialAdminEmail, "@") {
|
|
// Hubspot requires the timestamp to be rounded to the nearest day at midnight.
|
|
now := time.Now().UTC()
|
|
rounded := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
millis := rounded.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
|
go hubspotutil.SyncUser(pr.InitialAdminEmail, "", &hubspot.ContactProperties{IsServerAdmin: true, LatestPing: millis, HasAgreedToToS: pr.TosAccepted})
|
|
}
|
|
|
|
var clientAddr string
|
|
if v := r.Header.Get("x-forwarded-for"); v != "" {
|
|
clientAddr = v
|
|
} else {
|
|
clientAddr = r.RemoteAddr
|
|
}
|
|
|
|
message, err := marshalPing(pr, hasUpdate, clientAddr, time.Now())
|
|
if err != nil {
|
|
meter.ErrorCounter.Add(r.Context(), 1)
|
|
logger.Error("failed to marshal payload", log.Error(err))
|
|
return
|
|
}
|
|
|
|
err = pubsubClient.Publish(context.Background(), message)
|
|
if err != nil {
|
|
meter.ErrorCounter.Add(r.Context(), 1)
|
|
logger.Error("failed to publish", log.String("message", string(message)), log.Error(err))
|
|
return
|
|
}
|
|
}
|
|
|
|
func marshalPing(pr *pingRequest, hasUpdate bool, clientAddr string, now time.Time) ([]byte, error) {
|
|
codeIntelUsage, err := reserializeCodeIntelUsage(pr.NewCodeIntelUsage, pr.CodeIntelUsage)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "malformed code intel usage")
|
|
}
|
|
|
|
searchUsage, err := reserializeSearchUsage(pr.SearchUsage)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "malformed search usage")
|
|
}
|
|
|
|
return json.Marshal(&pingPayload{
|
|
RemoteIP: clientAddr,
|
|
RemoteSiteVersion: pr.ClientVersionString,
|
|
RemoteSiteID: pr.ClientSiteID,
|
|
LicenseKey: pr.LicenseKey,
|
|
ExternalURL: pr.ExternalURL,
|
|
Os: pr.Os,
|
|
HasUpdate: strconv.FormatBool(hasUpdate),
|
|
UniqueUsersToday: strconv.FormatInt(int64(pr.UniqueUsers), 10),
|
|
SiteActivity: pr.Activity, // no change in schema
|
|
BatchChangesUsage: pr.BatchChangesUsage, // no change in schema
|
|
NewCodeIntelUsage: codeIntelUsage,
|
|
SearchUsage: searchUsage,
|
|
GrowthStatistics: pr.GrowthStatistics,
|
|
SavedSearches: pr.SavedSearches,
|
|
HomepagePanels: pr.HomepagePanels,
|
|
RetentionStatistics: pr.RetentionStatistics,
|
|
Repositories: pr.Repositories,
|
|
RepositorySizeHistogram: pr.RepositorySizeHistogram,
|
|
SearchOnboarding: pr.SearchOnboarding,
|
|
InstallerEmail: pr.InitialAdminEmail,
|
|
DependencyVersions: pr.DependencyVersions,
|
|
ExtensionsUsage: pr.ExtensionsUsage,
|
|
CodeInsightsUsage: pr.CodeInsightsUsage,
|
|
CodeInsightsCriticalTelemetry: pr.CodeInsightsCriticalTelemetry,
|
|
SearchJobsUsage: pr.SearchJobsUsage,
|
|
CodeMonitoringUsage: pr.CodeMonitoringUsage,
|
|
NotebooksUsage: pr.NotebooksUsage,
|
|
CodeHostVersions: pr.CodeHostVersions,
|
|
CodeHostIntegrationUsage: pr.CodeHostIntegrationUsage,
|
|
IDEExtensionsUsage: pr.IDEExtensionsUsage,
|
|
OwnUsage: pr.OwnUsage,
|
|
AuthProviders: strings.Join(pr.AuthProviders, ","),
|
|
ExtServices: strings.Join(pr.ExternalServices, ","),
|
|
BuiltinSignupAllowed: strconv.FormatBool(pr.BuiltinSignupAllowed),
|
|
AccessRequestEnabled: strconv.FormatBool(pr.AccessRequestEnabled),
|
|
DeployType: pr.DeployType,
|
|
TotalUserAccounts: strconv.FormatInt(int64(pr.TotalUsers), 10),
|
|
TotalRepos: strconv.FormatInt(int64(pr.TotalRepos), 10),
|
|
HasExternalURL: strconv.FormatBool(pr.HasExtURL),
|
|
HasRepos: strconv.FormatBool(pr.HasRepos),
|
|
EverSearched: strconv.FormatBool(pr.EverSearched),
|
|
EverFindRefs: strconv.FormatBool(pr.EverFindRefs),
|
|
ActiveToday: strconv.FormatBool(pr.ActiveToday),
|
|
Timestamp: now.UTC().Format(time.RFC3339),
|
|
HasCodyEnabled: strconv.FormatBool(pr.HasCodyEnabled),
|
|
CodyUsage2: pr.CodyUsage2,
|
|
CodyProviders: pr.CodyProviders,
|
|
RepoMetadataUsage: pr.RepoMetadataUsage,
|
|
LlmUsage: pr.LlmUsage,
|
|
CodyContextFiltersConfigured: strconv.FormatBool(pr.CodyContextFiltersConfigured),
|
|
})
|
|
}
|
|
|
|
// reserializeCodeIntelUsage returns the given data in the shape of the current code intel
|
|
// usage statistics format. The given payload should be populated with either the new-style
|
|
func reserializeCodeIntelUsage(payload, fallbackPayload json.RawMessage) (json.RawMessage, error) {
|
|
if len(payload) != 0 {
|
|
return reserializeNewCodeIntelUsage(payload)
|
|
}
|
|
if len(fallbackPayload) != 0 {
|
|
return reserializeOldCodeIntelUsage(fallbackPayload)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func reserializeNewCodeIntelUsage(payload json.RawMessage) (json.RawMessage, error) {
|
|
var codeIntelUsage *types.NewCodeIntelUsageStatistics
|
|
if err := json.Unmarshal(payload, &codeIntelUsage); err != nil {
|
|
return nil, err
|
|
}
|
|
if codeIntelUsage == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var eventSummaries []jsonEventSummary
|
|
for _, event := range codeIntelUsage.EventSummaries {
|
|
eventSummaries = append(eventSummaries, translateEventSummary(event))
|
|
}
|
|
|
|
var investigationEvents []jsonCodeIntelInvestigationEvent
|
|
for _, event := range codeIntelUsage.InvestigationEvents {
|
|
investigationEvents = append(investigationEvents, translateInvestigationEvent(event))
|
|
}
|
|
|
|
countsByLanguage := make([]jsonCodeIntelRepositoryCountsByLanguage, 0, len(codeIntelUsage.CountsByLanguage))
|
|
for languageID, counts := range codeIntelUsage.CountsByLanguage {
|
|
countsByLanguage = append(countsByLanguage, jsonCodeIntelRepositoryCountsByLanguage{
|
|
LanguageID: &languageID,
|
|
NumRepositoriesWithUploadRecords: counts.NumRepositoriesWithUploadRecords,
|
|
NumRepositoriesWithFreshUploadRecords: counts.NumRepositoriesWithFreshUploadRecords,
|
|
NumRepositoriesWithIndexRecords: counts.NumRepositoriesWithIndexRecords,
|
|
NumRepositoriesWithFreshIndexRecords: counts.NumRepositoriesWithFreshIndexRecords,
|
|
})
|
|
}
|
|
sort.Slice(countsByLanguage, func(i, j int) bool {
|
|
return *countsByLanguage[i].LanguageID < *countsByLanguage[j].LanguageID
|
|
})
|
|
|
|
numRepositories := codeIntelUsage.NumRepositories
|
|
if numRepositories == nil && codeIntelUsage.NumRepositoriesWithUploadRecords != nil && codeIntelUsage.NumRepositoriesWithoutUploadRecords != nil {
|
|
val := *codeIntelUsage.NumRepositoriesWithUploadRecords + *codeIntelUsage.NumRepositoriesWithoutUploadRecords
|
|
numRepositories = &val
|
|
}
|
|
|
|
var numRepositoriesWithoutUploadRecords *int32
|
|
if codeIntelUsage.NumRepositories != nil && codeIntelUsage.NumRepositoriesWithUploadRecords != nil {
|
|
val := *codeIntelUsage.NumRepositories - *codeIntelUsage.NumRepositoriesWithUploadRecords
|
|
numRepositoriesWithoutUploadRecords = &val
|
|
}
|
|
|
|
languageRequests := make([]jsonLanguageRequest, 0, len(codeIntelUsage.LanguageRequests))
|
|
for _, request := range codeIntelUsage.LanguageRequests {
|
|
languageRequests = append(languageRequests, jsonLanguageRequest{
|
|
LanguageID: &request.LanguageID,
|
|
NumRequests: &request.NumRequests,
|
|
})
|
|
}
|
|
|
|
return json.Marshal(jsonCodeIntelUsage{
|
|
StartOfWeek: codeIntelUsage.StartOfWeek,
|
|
WAUs: codeIntelUsage.WAUs,
|
|
PreciseWAUs: codeIntelUsage.PreciseWAUs,
|
|
SearchBasedWAUs: codeIntelUsage.SearchBasedWAUs,
|
|
CrossRepositoryWAUs: codeIntelUsage.CrossRepositoryWAUs,
|
|
PreciseCrossRepositoryWAUs: codeIntelUsage.PreciseCrossRepositoryWAUs,
|
|
SearchBasedCrossRepositoryWAUs: codeIntelUsage.SearchBasedCrossRepositoryWAUs,
|
|
EventSummaries: eventSummaries,
|
|
NumRepositories: numRepositories,
|
|
NumRepositoriesWithUploadRecords: codeIntelUsage.NumRepositoriesWithUploadRecords,
|
|
NumRepositoriesWithoutUploadRecords: numRepositoriesWithoutUploadRecords,
|
|
NumRepositoriesWithFreshUploadRecords: codeIntelUsage.NumRepositoriesWithFreshUploadRecords,
|
|
NumRepositoriesWithIndexRecords: codeIntelUsage.NumRepositoriesWithIndexRecords,
|
|
NumRepositoriesWithFreshIndexRecords: codeIntelUsage.NumRepositoriesWithFreshIndexRecords,
|
|
NumRepositoriesWithIndexConfigurationRecords: codeIntelUsage.NumRepositoriesWithAutoIndexConfigurationRecords,
|
|
CountsByLanguage: countsByLanguage,
|
|
SettingsPageViewCount: codeIntelUsage.SettingsPageViewCount,
|
|
UsersWithRefPanelRedesignEnabled: codeIntelUsage.UsersWithRefPanelRedesignEnabled,
|
|
LanguageRequests: languageRequests,
|
|
InvestigationEvents: investigationEvents,
|
|
})
|
|
}
|
|
|
|
func reserializeOldCodeIntelUsage(payload json.RawMessage) (json.RawMessage, error) {
|
|
var codeIntelUsage *types.OldCodeIntelUsageStatistics
|
|
if err := json.Unmarshal(payload, &codeIntelUsage); err != nil {
|
|
return nil, err
|
|
}
|
|
if codeIntelUsage == nil || len(codeIntelUsage.Weekly) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
unwrap := func(i *int32) int32 {
|
|
if i == nil {
|
|
return 0
|
|
}
|
|
return *i
|
|
}
|
|
|
|
week := codeIntelUsage.Weekly[0]
|
|
hover := week.Hover
|
|
definitions := week.Definitions
|
|
references := week.References
|
|
|
|
eventSummaries := []jsonEventSummary{
|
|
{Action: "hover", Source: "precise", WAUs: hover.LSIF.UsersCount, TotalActions: unwrap(hover.LSIF.EventsCount)},
|
|
{Action: "hover", Source: "search", WAUs: hover.Search.UsersCount, TotalActions: unwrap(hover.Search.EventsCount)},
|
|
{Action: "definitions", Source: "precise", WAUs: definitions.LSIF.UsersCount, TotalActions: unwrap(definitions.LSIF.EventsCount)},
|
|
{Action: "definitions", Source: "search", WAUs: definitions.Search.UsersCount, TotalActions: unwrap(definitions.Search.EventsCount)},
|
|
{Action: "references", Source: "precise", WAUs: references.LSIF.UsersCount, TotalActions: unwrap(references.LSIF.EventsCount)},
|
|
{Action: "references", Source: "search", WAUs: references.Search.UsersCount, TotalActions: unwrap(references.Search.EventsCount)},
|
|
}
|
|
|
|
return json.Marshal(jsonCodeIntelUsage{
|
|
StartOfWeek: week.StartTime,
|
|
EventSummaries: eventSummaries,
|
|
})
|
|
}
|
|
|
|
type jsonCodeIntelUsage struct {
|
|
StartOfWeek time.Time `json:"start_time"`
|
|
WAUs *int32 `json:"waus"`
|
|
PreciseWAUs *int32 `json:"precise_waus"`
|
|
SearchBasedWAUs *int32 `json:"search_waus"`
|
|
CrossRepositoryWAUs *int32 `json:"xrepo_waus"`
|
|
PreciseCrossRepositoryWAUs *int32 `json:"precise_xrepo_waus"`
|
|
SearchBasedCrossRepositoryWAUs *int32 `json:"search_xrepo_waus"`
|
|
EventSummaries []jsonEventSummary `json:"event_summaries"`
|
|
NumRepositories *int32 `json:"num_repositories"`
|
|
NumRepositoriesWithUploadRecords *int32 `json:"num_repositories_with_upload_records"`
|
|
NumRepositoriesWithoutUploadRecords *int32 `json:"num_repositories_without_upload_records"`
|
|
NumRepositoriesWithFreshUploadRecords *int32 `json:"num_repositories_with_fresh_upload_records"`
|
|
NumRepositoriesWithIndexRecords *int32 `json:"num_repositories_with_index_records"`
|
|
NumRepositoriesWithFreshIndexRecords *int32 `json:"num_repositories_with_fresh_index_records"`
|
|
NumRepositoriesWithIndexConfigurationRecords *int32 `json:"num_repositories_with_index_configuration_records"`
|
|
CountsByLanguage []jsonCodeIntelRepositoryCountsByLanguage `json:"counts_by_language"`
|
|
SettingsPageViewCount *int32 `json:"settings_page_view_count"`
|
|
UsersWithRefPanelRedesignEnabled *int32 `json:"users_with_ref_panel_redesign_enabled"`
|
|
LanguageRequests []jsonLanguageRequest `json:"language_requests"`
|
|
InvestigationEvents []jsonCodeIntelInvestigationEvent `json:"investigation_events"`
|
|
}
|
|
|
|
type jsonCodeIntelRepositoryCountsByLanguage struct {
|
|
LanguageID *string `json:"language_id"`
|
|
NumRepositoriesWithUploadRecords *int32 `json:"num_repositories_with_upload_records"`
|
|
NumRepositoriesWithFreshUploadRecords *int32 `json:"num_repositories_with_fresh_upload_records"`
|
|
NumRepositoriesWithIndexRecords *int32 `json:"num_repositories_with_index_records"`
|
|
NumRepositoriesWithFreshIndexRecords *int32 `json:"num_repositories_with_fresh_index_records"`
|
|
}
|
|
|
|
type jsonLanguageRequest struct {
|
|
LanguageID *string `json:"language_id"`
|
|
NumRequests *int32 `json:"num_requests"`
|
|
}
|
|
|
|
type jsonCodeIntelInvestigationEvent struct {
|
|
Type string `json:"type"`
|
|
WAUs int32 `json:"waus"`
|
|
Total int32 `json:"total"`
|
|
}
|
|
|
|
type jsonEventSummary struct {
|
|
Action string `json:"action"`
|
|
Source string `json:"source"`
|
|
LanguageID string `json:"language_id"`
|
|
CrossRepository bool `json:"cross_repository"`
|
|
WAUs int32 `json:"waus"`
|
|
TotalActions int32 `json:"total_actions"`
|
|
}
|
|
|
|
var codeIntelActionNames = map[types.CodeIntelAction]string{
|
|
types.HoverAction: "hover",
|
|
types.DefinitionsAction: "definitions",
|
|
types.ReferencesAction: "references",
|
|
}
|
|
|
|
var codeIntelSourceNames = map[types.CodeIntelSource]string{
|
|
types.PreciseSource: "precise",
|
|
types.SearchSource: "search",
|
|
}
|
|
|
|
var codeIntelInvestigationTypeNames = map[types.CodeIntelInvestigationType]string{
|
|
types.CodeIntelIndexerSetupInvestigationType: "CodeIntelligenceIndexerSetupInvestigated",
|
|
types.CodeIntelUploadErrorInvestigationType: "CodeIntelligenceUploadErrorInvestigated",
|
|
types.CodeIntelIndexErrorInvestigationType: "CodeIntelligenceIndexErrorInvestigated",
|
|
}
|
|
|
|
func translateEventSummary(event types.CodeIntelEventSummary) jsonEventSummary {
|
|
return jsonEventSummary{
|
|
Action: codeIntelActionNames[event.Action],
|
|
Source: codeIntelSourceNames[event.Source],
|
|
LanguageID: event.LanguageID,
|
|
CrossRepository: event.CrossRepository,
|
|
WAUs: event.WAUs,
|
|
TotalActions: event.TotalActions,
|
|
}
|
|
}
|
|
|
|
func translateInvestigationEvent(event types.CodeIntelInvestigationEvent) jsonCodeIntelInvestigationEvent {
|
|
return jsonCodeIntelInvestigationEvent{
|
|
Type: codeIntelInvestigationTypeNames[event.Type],
|
|
WAUs: event.WAUs,
|
|
Total: event.Total,
|
|
}
|
|
}
|
|
|
|
// reserializeSearchUsage will reserialize a code intel usage statistics
|
|
// struct with only the first period in each period type. This reduces the
|
|
// complexity required in the BigQuery schema and downstream ETL transform
|
|
// logic.
|
|
func reserializeSearchUsage(payload json.RawMessage) (json.RawMessage, error) {
|
|
if len(payload) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var searchUsage *types.SearchUsageStatistics
|
|
if err := json.Unmarshal(payload, &searchUsage); err != nil {
|
|
return nil, err
|
|
}
|
|
if searchUsage == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
singlePeriodUsage := struct {
|
|
Daily *types.SearchUsagePeriod
|
|
Weekly *types.SearchUsagePeriod
|
|
Monthly *types.SearchUsagePeriod
|
|
}{}
|
|
|
|
if len(searchUsage.Daily) > 0 {
|
|
singlePeriodUsage.Daily = searchUsage.Daily[0]
|
|
}
|
|
if len(searchUsage.Weekly) > 0 {
|
|
singlePeriodUsage.Weekly = searchUsage.Weekly[0]
|
|
}
|
|
if len(searchUsage.Monthly) > 0 {
|
|
singlePeriodUsage.Monthly = searchUsage.Monthly[0]
|
|
}
|
|
|
|
return json.Marshal(singlePeriodUsage)
|
|
}
|