Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/analytics/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
configMediator "github.com/ActiveState/cli/internal/mediators/config"
)

func init() {
configMediator.RegisterOption(constants.AnalyticsPixelOverrideConfig, configMediator.String, "")
}

// Dispatcher describes a struct that can send analytics event in the background
type Dispatcher interface {
Event(category, action string, dim ...*dimensions.Values)
Expand Down
6 changes: 3 additions & 3 deletions internal/analytics/client/sync/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ func New(source string, cfg *config.Instance, auth *authentication.Auth, out out
// Register reporters
if condition.InTest() {
logging.Debug("Using test reporter")
a.NewReporter(reporters.NewTestReporter(reporters.TestReportFilepath()))
a.NewReporter(reporters.NewTestReporter(reporters.TestReportFilepath(), a.cfg))
logging.Debug("Using test reporter as instructed by env")
} else if v := os.Getenv(constants.AnalyticsLogEnvVarName); v != "" {
a.NewReporter(reporters.NewTestReporter(v))
a.NewReporter(reporters.NewTestReporter(v, a.cfg))
} else {
a.NewReporter(reporters.NewPixelReporter())
a.NewReporter(reporters.NewPixelReporter(a.cfg))
}

return a
Expand Down
38 changes: 31 additions & 7 deletions internal/analytics/client/sync/reporters/pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@ import (
"os"

"github.com/ActiveState/cli/internal/analytics/dimensions"
"github.com/ActiveState/cli/internal/config"
"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/errs"

configMediator "github.com/ActiveState/cli/internal/mediators/config"
)

type PixelReporter struct {
url string
}

func NewPixelReporter() *PixelReporter {
var pixelUrl string

// Attempt to get the value for the pixel URL from the environment. Fall back to default if that fails
if pixelUrl = os.Getenv(constants.AnalyticsPixelOverrideEnv); pixelUrl == "" {
pixelUrl = constants.DefaultAnalyticsPixel
func NewPixelReporter(cfg *config.Instance) *PixelReporter {
reporter := &PixelReporter{
url: sourcePixelURL(cfg),
}
return &PixelReporter{pixelUrl}

configMediator.AddListener(constants.AnalyticsPixelOverrideConfig, func() {
reporter.url = sourcePixelURL(cfg)
})
return reporter
}

func (r *PixelReporter) ID() string {
Expand Down Expand Up @@ -55,3 +59,23 @@ func (r *PixelReporter) Event(category, action, source, label string, d *dimensi

return nil
}

func sourcePixelURL(cfg *config.Instance) string {
var (
pixelUrl string

envUrl = os.Getenv(constants.AnalyticsPixelOverrideEnv)
cfgUrl = cfg.GetString(constants.AnalyticsPixelOverrideConfig)
)

switch {
case envUrl != "":
pixelUrl = envUrl
case cfgUrl != "":
pixelUrl = cfgUrl
default:
pixelUrl = constants.DefaultAnalyticsPixel
}

return pixelUrl
}
16 changes: 13 additions & 3 deletions internal/analytics/client/sync/reporters/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import (
"path/filepath"

"github.com/ActiveState/cli/internal/analytics/dimensions"
"github.com/ActiveState/cli/internal/config"
"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/fileutils"
"github.com/ActiveState/cli/internal/installation/storage"
"github.com/ActiveState/cli/internal/logging"
configMediator "github.com/ActiveState/cli/internal/mediators/config"
)

type TestReporter struct {
path string
cfg *config.Instance
}

const TestReportFilename = "analytics.log"
Expand All @@ -23,8 +27,12 @@ func TestReportFilepath() string {
return filepath.Join(appdata, TestReportFilename)
}

func NewTestReporter(path string) *TestReporter {
return &TestReporter{path}
func NewTestReporter(path string, cfg *config.Instance) *TestReporter {
reporter := &TestReporter{path, cfg}
configMediator.AddListener(constants.AnalyticsPixelOverrideConfig, func() {
reporter.cfg = cfg
})
return reporter
}

func (r *TestReporter) ID() string {
Expand All @@ -36,11 +44,13 @@ type TestLogEntry struct {
Action string
Source string
Label string
URL string
Dimensions *dimensions.Values
}

func (r *TestReporter) Event(category, action, source, label string, d *dimensions.Values) error {
b, err := json.Marshal(TestLogEntry{category, action, source, label, d})
url := r.cfg.GetString(constants.AnalyticsPixelOverrideConfig)
b, err := json.Marshal(TestLogEntry{category, action, source, label, url, d})
if err != nil {
return errs.Wrap(err, "Could not marshal test log entry")
}
Expand Down
3 changes: 3 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,9 @@ const SecurityPromptConfig = "security.prompt.enabled"
// SecurityPromptLevelConfig is the config key used to determine the level of security prompts
const SecurityPromptLevelConfig = "security.prompt.level"

// AnalyticsPixelOverrideConfig is the config key used to override the analytics pixel url
const AnalyticsPixelOverrideConfig = "report.analytics.endpoint"

// UpdateEndpointConfig is the config key used to determine the update endpoint to use
const UpdateEndpointConfig = "update.endpoint"

Expand Down
78 changes: 78 additions & 0 deletions test/integration/analytics_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import (

"github.com/ActiveState/termtest"
"github.com/thoas/go-funk"
"golang.org/x/net/context"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/ipc"
"github.com/ActiveState/cli/internal/runbits/runtime/trigger"
"github.com/ActiveState/cli/internal/svcctl"
"github.com/ActiveState/cli/internal/testhelpers/suite"
"github.com/ActiveState/cli/pkg/platform/model"

"github.com/ActiveState/cli/internal/analytics/client/sync/reporters"
anaConst "github.com/ActiveState/cli/internal/analytics/constants"
Expand Down Expand Up @@ -644,6 +649,79 @@ func (suite *AnalyticsIntegrationTestSuite) TestCIAndInteractiveDimensions() {
}
}

func (suite *AnalyticsIntegrationTestSuite) TestAnalyticsPixelOverride() {
suite.OnlyRunForTags(tagsuite.Analytics)

ts := e2e.New(suite.T(), false)
defer ts.Close()

testURL := "https://example.com"
cp := ts.Spawn("config", "set", constants.AnalyticsPixelOverrideConfig, testURL)
cp.Expect("Successfully set config key")
cp.ExpectExitCode(0)

// Create IPC client using the test's socket directory to connect to the same state-svc instance
// that the spawned state tool commands are using.
// We make a request to the service directly to ensure we're hitting an endpoint that will send an event.
sockPath := &ipc.SockPath{
RootDir: ts.Dirs.SockRoot,
AppName: constants.CommandName,
AppChannel: constants.ChannelName,
}
ipcClient := ipc.NewClient(sockPath)

svcPort, err := svcctl.LocateHTTP(ipcClient)
suite.Require().NoError(err, errs.JoinMessage(err))

svcmodel := model.NewSvcModel(svcPort)
_, err = svcmodel.LocalProjects(context.Background())
suite.Require().NoError(err, errs.JoinMessage(err))

time.Sleep(time.Second) // Ensure state-svc has time to report events

suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename)
events := parseAnalyticsEvents(suite, ts)
suite.Require().NotEmpty(events)

// Some events will fire before the config is updated, so we expect to
// find at least one event with the new configuration values after the service is restarted.
found := false
for _, e := range events {
// Specifically check an event sent via the state-svc and ensure that the URL is the one we set in the config
if e.Category == anaConst.CatStateSvc && e.Action == "endpoint" && e.Label == "Projects" {
suite.Assert().Equal(testURL, e.URL)
found = true
}
}
suite.Assert().True(found, "Should find at least one state-svc endpoint Projects event")

// Check that all events after the config set event have the correct URL.
configSetEventIndex := -1
for i, e := range events {
if e.Category == anaConst.CatConfig && e.Action == anaConst.ActConfigSet && e.Label == constants.AnalyticsPixelOverrideConfig {
configSetEventIndex = i
break
}
}
suite.Require().NotEqual(-1, configSetEventIndex, "Should find the config set event")

// Check all events after config set, skipping the immediate update event
for i, e := range events {
if i > configSetEventIndex {
// Skip the immediate update event right after config set.
// The update event is sent before the config is updated, so it will have an empty URL.
if i == configSetEventIndex+1 && e.Category == "updates" {
continue
}
// All other events with URLs should have the correct one.
// This includes events from the state tool and the state-svc.
if e.URL != testURL {
suite.Equal(testURL, e.URL, "Event after config set (%s:%s) should have the updated URL", e.Category, e.Action)
}
}
}
}

func TestAnalyticsIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(AnalyticsIntegrationTestSuite))
}
Loading