Skip to content

Commit

Permalink
Tool version telemetry (#903)
Browse files Browse the repository at this point in the history
<!--
  Thanks for contributing to the Bitrise CLI!
  Please fill this template with the details of your change.
-->

### Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. This is simply a reminder of what we are going to look
  for before merging your code.
-->
- [ ] I've read and followed the [Contribution Guidelines](https://github.com/bitrise-io/bitrise/blob/master/.github/CONTRIBUTING.md)
- [ ] `README.md` is updated with the changes (if needed)

### Version
<!-- Leave this untouched if you don't know, we'll help -->
Requires a *MAJOR/MINOR/PATCH* [version update](https://semver.org/)

### Context

Follow up to #902 

### Changes

- Send the collected tool version report as a standard telemetry event.
- Add a few more useful properties to the event

### Investigation details

<!-- Please share any alternative solutions that were considered along with investigation details. -->

### Decisions

As the tool version map contains arbitrary key-value pairs and we'll add more tools under ASDF, I decided to just serialize everything as JSON. We can process the JSON at query-time easily.
  • Loading branch information
ofalvai authored Jan 8, 2024
1 parent 17c0537 commit cc732b1
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 44 deletions.
3 changes: 0 additions & 3 deletions analytics/state_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const (
trueEnv = "true"
)

// StateChecker ...
type StateChecker interface {
Enabled() bool
UseAsync() bool
Expand All @@ -25,12 +24,10 @@ type stateChecker struct {
envRepository env.Repository
}

// NewStateChecker ...
func NewStateChecker(repository env.Repository) StateChecker {
return stateChecker{envRepository: repository}
}

// Enabled ...
func (s stateChecker) Enabled() bool {
if s.envRepository.Get(V2DisabledEnvKey) == trueEnv {
return false
Expand Down
82 changes: 51 additions & 31 deletions analytics/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"runtime"
"time"

"github.com/bitrise-io/bitrise/configs"
Expand All @@ -12,15 +13,13 @@ import (
"github.com/bitrise-io/bitrise/version"
"github.com/bitrise-io/go-utils/v2/analytics"
"github.com/bitrise-io/go-utils/v2/env"
utilslog "github.com/bitrise-io/go-utils/v2/log"
)

const (
// BuildExecutionID ...
BuildExecutionID = "build_execution_id"
// WorkflowExecutionID ...
BuildExecutionID = "build_execution_id"
WorkflowExecutionID = "workflow_execution_id"
// StepExecutionID ...
StepExecutionID = "step_execution_id"
StepExecutionID = "step_execution_id"

workflowStartedEventName = "workflow_started"
workflowFinishedEventName = "workflow_finished"
Expand All @@ -30,6 +29,7 @@ const (
stepPreparationFailedEventName = "step_preparation_failed"
stepSkippedEventName = "step_skipped"
cliWarningEventName = "cli_warning"
toolVersionSnapshotEventName = "tool_version_snapshot"

workflowNameProperty = "workflow_name"
workflowTitleProperty = "workflow_title"
Expand All @@ -55,31 +55,36 @@ const (
stepSourceProperty = "step_source"
skippableProperty = "skippable"
timeoutProperty = "timeout"
runtime = "runtime"

failedValue = "failed"
successfulValue = "successful"
buildFailedValue = "build_failed"
runIfValue = "run_if"
customTimeoutValue = "timeout"
noOutputTimeoutValue = "no_output_timeout"

buildSlugEnvKey = "BITRISE_BUILD_SLUG"
appSlugEnvKey = "BITRISE_APP_SLUG"
runTimeProperty = "runtime"
osProperty = "os"
stackRevIdProperty = "stack_rev_id"
snapshotProperty = "snapshot"
toolVersionsProperty = "tool_versions"

failedValue = "failed"
successfulValue = "successful"
buildFailedValue = "build_failed"
runIfValue = "run_if"
customTimeoutValue = "timeout"
noOutputTimeoutValue = "no_output_timeout"
ToolSnapshotEndOfWorkflowValue = "end_of_workflow"

buildSlugEnvKey = "BITRISE_BUILD_SLUG"
appSlugEnvKey = "BITRISE_APP_SLUG"
StepExecutionIDEnvKey = "BITRISE_STEP_EXECUTION_ID"
stackRevIdKey = "BITRISE_STACK_REV_ID"
macStackRevIdKey = "BITRISE_OSX_STACK_REV_ID"

bitriseVersionKey = "bitrise"
envmanVersionKey = "envman"
stepmanVersionKey = "stepman"
)

// Input ...
type Input struct {
Value interface{} `json:"value"`
OriginalValue string `json:"original_value,omitempty"`
}

// StepInfo ...
type StepInfo struct {
StepID string
StepTitle string
Expand All @@ -88,7 +93,6 @@ type StepInfo struct {
Skippable bool
}

// StepResult ...
type StepResult struct {
Info StepInfo
Status models.StepRunStatus
Expand All @@ -97,28 +101,27 @@ type StepResult struct {
Runtime time.Duration
}

// Tracker ...
type Tracker interface {
SendWorkflowStarted(properties analytics.Properties, name string, title string)
SendWorkflowFinished(properties analytics.Properties, failed bool)
SendStepStartedEvent(properties analytics.Properties, info StepInfo, expandedInputs map[string]interface{}, originalInputs map[string]string)
SendStepFinishedEvent(properties analytics.Properties, result StepResult)
SendCLIWarning(message string)
SendToolVersionSnapshot(toolVersions, snapshotType string)
Wait()
}

type tracker struct {
tracker analytics.Tracker
envRepository env.Repository
stateChecker StateChecker
logger utilslog.Logger
}

// NewTracker ...
func NewTracker(analyticsTracker analytics.Tracker, envRepository env.Repository, stateChecker StateChecker) Tracker {
return tracker{tracker: analyticsTracker, envRepository: envRepository, stateChecker: stateChecker}
func NewTracker(analyticsTracker analytics.Tracker, envRepository env.Repository, stateChecker StateChecker, logger utilslog.Logger) Tracker {
return tracker{tracker: analyticsTracker, envRepository: envRepository, stateChecker: stateChecker, logger: logger}
}

// NewDefaultTracker ...
func NewDefaultTracker() Tracker {
envRepository := env.NewRepository()
stateChecker := NewStateChecker(envRepository)
Expand All @@ -129,7 +132,7 @@ func NewDefaultTracker() Tracker {
tracker = analytics.NewDefaultTracker(&logger)
}

return NewTracker(tracker, envRepository, stateChecker)
return NewTracker(tracker, envRepository, stateChecker, &logger)
}

// SendWorkflowStarted sends `workflow_started` events. `parent_step_execution_id` can be used to filter those
Expand Down Expand Up @@ -187,7 +190,6 @@ func (t tracker) SendWorkflowStarted(properties analytics.Properties, name strin
t.tracker.Enqueue(workflowStartedEventName, properties, stateProperties)
}

// SendWorkflowFinished ...
func (t tracker) SendWorkflowFinished(properties analytics.Properties, failed bool) {
if !t.stateChecker.Enabled() {
return
Expand All @@ -203,7 +205,28 @@ func (t tracker) SendWorkflowFinished(properties analytics.Properties, failed bo
t.tracker.Enqueue(workflowFinishedEventName, properties, analytics.Properties{statusProperty: statusMessage})
}

// SendStepStartedEvent ...
func (t tracker) SendToolVersionSnapshot(toolVersions, snapshotType string) {
if !t.stateChecker.Enabled() {
return
}

stackRevId := t.envRepository.Get(stackRevIdKey)
if stackRevId == "" {
// Legacy
stackRevId = t.envRepository.Get(macStackRevIdKey)
}

properties := analytics.Properties{
ciModeProperty: t.envRepository.Get(configs.CIModeEnvKey) == "true",
osProperty: runtime.GOOS,
stackRevIdProperty: stackRevId,
snapshotProperty: snapshotType,
toolVersionsProperty: toolVersions,
}

t.tracker.Enqueue(toolVersionSnapshotEventName, properties)
}

func (t tracker) SendStepStartedEvent(properties analytics.Properties, info StepInfo, expandedInputs map[string]interface{}, originalInputs map[string]string) {
if !t.stateChecker.Enabled() {
return
Expand All @@ -230,7 +253,6 @@ func (t tracker) SendStepStartedEvent(properties analytics.Properties, info Step
t.tracker.Enqueue(stepStartedEventName, extraProperties...)
}

// SendStepFinishedEvent ...
func (t tracker) SendStepFinishedEvent(properties analytics.Properties, result StepResult) {
if !t.stateChecker.Enabled() {
return
Expand All @@ -244,7 +266,6 @@ func (t tracker) SendStepFinishedEvent(properties analytics.Properties, result S
t.tracker.Enqueue(eventName, properties, extraProperties)
}

// SendCLIWarning ...
func (t tracker) SendCLIWarning(message string) {
if !t.stateChecker.Enabled() {
return
Expand All @@ -253,7 +274,6 @@ func (t tracker) SendCLIWarning(message string) {
t.tracker.Enqueue(cliWarningEventName, analytics.Properties{messageProperty: message})
}

// Wait ...
func (t tracker) Wait() {
t.tracker.Wait()
}
Expand Down Expand Up @@ -314,7 +334,7 @@ func mapStepResultToEvent(result StepResult) (string, analytics.Properties, erro
return "", analytics.Properties{}, fmt.Errorf("Unknown step status code: %d", result.Status)
}

extraProperties[runtime] = int64(result.Runtime.Seconds())
extraProperties[runTimeProperty] = int64(result.Runtime.Seconds())

return eventName, extraProperties, nil
}
1 change: 1 addition & 0 deletions cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1730,4 +1730,5 @@ func (n noOpTracker) SendStepFinishedEvent(analytics.Properties, cliAnalytics.St
func (n noOpTracker) SendCLIWarning(string) {}
func (n noOpTracker) SendWorkflowStarted(analytics.Properties, string, string) {}
func (n noOpTracker) SendWorkflowFinished(analytics.Properties, bool) {}
func (n noOpTracker) SendToolVersionSnapshot(string, string) {}
func (n noOpTracker) Wait() {}
32 changes: 32 additions & 0 deletions cli/run_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/bitrise-io/bitrise/stepruncmd"
"github.com/bitrise-io/bitrise/toolkits"
"github.com/bitrise-io/bitrise/tools"
"github.com/bitrise-io/bitrise/toolversions"
envman "github.com/bitrise-io/envman/cli"
"github.com/bitrise-io/envman/env"
envmanEnv "github.com/bitrise-io/envman/env"
Expand All @@ -33,6 +34,8 @@ import (
"github.com/bitrise-io/go-utils/pointers"
"github.com/bitrise-io/go-utils/retry"
coreanalytics "github.com/bitrise-io/go-utils/v2/analytics"
commandV2 "github.com/bitrise-io/go-utils/v2/command"
envV2 "github.com/bitrise-io/go-utils/v2/env"
logV2 "github.com/bitrise-io/go-utils/v2/log"
"github.com/bitrise-io/go-utils/versions"
stepmanModels "github.com/bitrise-io/stepman/models"
Expand Down Expand Up @@ -877,9 +880,38 @@ func (r WorkflowRunner) runWorkflow(
*environments = append(*environments, workflow.Environments...)
results := r.activateAndRunSteps(plan, workflow, steplibSource, buildRunResults, environments, secrets, isLastWorkflow, tracker, workflowIDProperties)
tracker.SendWorkflowFinished(workflowIDProperties, results.IsBuildFailed())
collectToolVersions(tracker)
return results
}

func collectToolVersions(tracker analytics.Tracker) {
userHomeDir, err := os.UserHomeDir()
if err != nil {
log.Warnf("user home dir not found: %w", err)
}

logger := log.NewLogger(log.GetGlobalLoggerOpts())
reporter := toolversions.NewASDFVersionReporter(envV2.NewCommandLocator(), commandV2.NewFactory(envV2.NewRepository()), logger, userHomeDir)

if !reporter.IsAvailable() {
log.Debugf("ASDF is not available, skipping tool version reporting")
return
}

toolVersions, err := reporter.CurrentToolVersions()
if err != nil {
log.Warnf("Tool version reporting: %s", err)
return
}
toolVersionsBytes, err := json.Marshal(toolVersions)
if err != nil {
logger.Warnf("Tool version reporting: JSON marshal: %s", err)
return
}

tracker.SendToolVersionSnapshot(string(toolVersionsBytes), analytics.ToolSnapshotEndOfWorkflowValue)
}

func addTestMetadata(testDirPath string, testResultStepInfo models.TestResultStepInfo) error {
// check if the test dir is empty
if empty, err := isDirEmpty(testDirPath); err != nil {
Expand Down
10 changes: 5 additions & 5 deletions toolversions/toolversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

"github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/env"
"github.com/bitrise-io/go-utils/v2/log"
"github.com/bitrise-io/bitrise/log"
)

type ToolVersionReporter interface {
Expand All @@ -22,10 +22,10 @@ type ToolVersionReporter interface {
}

type ToolVersion struct {
Version string
IsInstalled bool
DeclaredByFile string
IsGlobal bool
Version string `json:"version"`
IsInstalled bool `json:"is_installed"`
DeclaredByFile string `json:"declared_by_file"`
IsGlobal bool `json:"is_global"`
}

type ASDFVersionReporter struct {
Expand Down
8 changes: 3 additions & 5 deletions toolversions/toolversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"

"github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/log"
"github.com/bitrise-io/bitrise/log"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -52,8 +52,7 @@ func TestIsAvailable(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := log.NewLogger()
logger.EnableDebugLog(true)
logger := log.NewLogger(log.GetGlobalLoggerOpts())
r := NewASDFVersionReporter(
fakeCommandLocator{path: tt.systemPath},
fakeCommandFactory{stdout: tt.cmdOutput, exitCode: tt.cmdExitCode},
Expand Down Expand Up @@ -128,8 +127,7 @@ func TestCurrentToolVersions(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := log.NewLogger()
logger.EnableDebugLog(true)
logger := log.NewLogger(log.GetGlobalLoggerOpts())
r := NewASDFVersionReporter(
fakeCommandLocator{path: "/root/.asdf/bin/asdf"},
fakeCommandFactory{stdout: tt.cmdOutput},
Expand Down

0 comments on commit cc732b1

Please sign in to comment.