github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/analytics_int_test.go (about) 1 package integration 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "path/filepath" 7 "runtime" 8 "sort" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/ActiveState/cli/internal/testhelpers/suite" 14 "github.com/ActiveState/termtest" 15 "github.com/thoas/go-funk" 16 17 "github.com/ActiveState/cli/internal/analytics/client/sync/reporters" 18 anaConst "github.com/ActiveState/cli/internal/analytics/constants" 19 "github.com/ActiveState/cli/internal/condition" 20 "github.com/ActiveState/cli/internal/constants" 21 "github.com/ActiveState/cli/internal/fileutils" 22 "github.com/ActiveState/cli/internal/rtutils" 23 "github.com/ActiveState/cli/internal/testhelpers/e2e" 24 helperSuite "github.com/ActiveState/cli/internal/testhelpers/suite" 25 "github.com/ActiveState/cli/internal/testhelpers/tagsuite" 26 "github.com/ActiveState/cli/pkg/platform/runtime/target" 27 ) 28 29 type AnalyticsIntegrationTestSuite struct { 30 tagsuite.Suite 31 eventsfile string 32 } 33 34 // TestHeartbeats ensures that heartbeats are send on runtime use 35 // Note the heartbeat code especially is a little awkward as we have to account for timing offsets between state and 36 // state-svc. For that reason we tend to assert "greater than" rather than equals, because checking for equals introduces 37 // race conditions into the testing suite (not the state tool itself). 38 func (suite *AnalyticsIntegrationTestSuite) TestHeartbeats() { 39 suite.OnlyRunForTags(tagsuite.Analytics, tagsuite.Critical) 40 41 /* TEST SETUP */ 42 43 ts := e2e.New(suite.T(), true) 44 defer ts.Close() 45 46 namespace := "ActiveState-CLI/Alternate-Python" 47 commitID := "efcc851f-1451-4d0a-9dcb-074ac3f35f0a" 48 49 // We want to do a clean test without an activate event, so we have to manually seed the yaml 50 ts.PrepareProject(namespace, commitID) 51 52 heartbeatInterval := 1000 // in milliseconds 53 sleepTime := time.Duration(heartbeatInterval) * time.Millisecond 54 sleepTime = sleepTime + (sleepTime / 2) 55 56 env := []string{ 57 constants.DisableRuntime + "=false", 58 fmt.Sprintf("%s=%d", constants.HeartbeatIntervalEnvVarName, heartbeatInterval), 59 } 60 61 /* ACTIVATE TESTS */ 62 63 // Produce Activate Heartbeats 64 65 var cp *e2e.SpawnedCmd 66 if runtime.GOOS == "windows" { 67 cp = ts.SpawnCmdWithOpts("cmd.exe", 68 e2e.OptArgs("/k", "state", "activate"), 69 e2e.OptWD(ts.Dirs.Work), 70 e2e.OptAppendEnv(env...), 71 ) 72 } else { 73 cp = ts.SpawnWithOpts(e2e.OptArgs("activate"), 74 e2e.OptWD(ts.Dirs.Work), 75 e2e.OptAppendEnv(env...), 76 ) 77 } 78 79 cp.Expect("Creating a Virtual Environment") 80 cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) 81 cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) 82 83 time.Sleep(time.Second) // Ensure state-svc has time to report events 84 85 // By this point the activate heartbeats should have been recorded 86 87 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 88 89 events := parseAnalyticsEvents(suite, ts) 90 91 // Now it's time for us to assert that we are seeing the expected number of events 92 93 suite.Require().NotEmpty(events) 94 95 // Runtime:start events 96 suite.assertNEvents(events, 1, anaConst.CatRuntimeDebug, anaConst.ActRuntimeStart, anaConst.SrcStateTool, 97 fmt.Sprintf("output:\n%s\n%s", 98 cp.Output(), ts.DebugLogsDump())) 99 100 // Runtime:success events 101 suite.assertNEvents(events, 1, anaConst.CatRuntimeDebug, anaConst.ActRuntimeSuccess, anaConst.SrcStateTool, 102 fmt.Sprintf("output:\n%s\n%s", 103 cp.Output(), ts.DebugLogsDump())) 104 105 // Runtime-use:attempts events 106 attemptInitialCount := countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeAttempt, anaConst.SrcStateTool) 107 suite.Equal(1, attemptInitialCount, "Activate should have resulted in 1 attempt") 108 109 // Runtime-use:heartbeat events 110 heartbeatInitialCount := countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcStateTool) 111 if heartbeatInitialCount < 2 { 112 // It's possible due to the timing of the heartbeats and the fact that they are async that we have gotten either 113 // one or two by this point. Technically more is possible, just very unlikely. 114 suite.Fail(fmt.Sprintf("Received %d heartbeats, realistically we should at least have gotten 2", heartbeatInitialCount)) 115 } 116 117 // Wait for additional heartbeats to be reported, because our activated shell is still open 118 119 time.Sleep(sleepTime) 120 121 events = parseAnalyticsEvents(suite, ts) 122 suite.Require().NotEmpty(events) 123 124 suite.assertNEvents(events, 1, anaConst.CatRuntimeUsage, anaConst.ActRuntimeAttempt, anaConst.SrcStateTool, "Should still only have 1 attempt") 125 126 // Runtime-use:heartbeat events - should now be at least +1 because we waited <heartbeatInterval> 127 suite.assertGtEvents(events, heartbeatInitialCount, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcStateTool, 128 fmt.Sprintf("output:\n%s\n%s", 129 cp.Output(), ts.DebugLogsDump())) 130 131 /* EXECUTOR TESTS */ 132 133 // Test that executor is sending heartbeats 134 suite.Run("Executors", func() { 135 cp.SendLine("python3 -c \"import sys; print(sys.copyright)\"") 136 cp.Expect("provided by ActiveState") 137 138 time.Sleep(sleepTime) 139 140 eventsAfterExecutor := parseAnalyticsEvents(suite, ts) 141 suite.Require().Greater(len(eventsAfterExecutor), len(events), "Should have received more events after running executor") 142 143 executorEvents := filterEvents(eventsAfterExecutor, func(e reporters.TestLogEntry) bool { 144 if e.Dimensions == nil || e.Dimensions.Trigger == nil { 145 return false 146 } 147 return (*e.Dimensions.Trigger) == target.TriggerExecutor.String() 148 }) 149 suite.Require().Equal(1, countEvents(executorEvents, anaConst.CatRuntimeUsage, anaConst.ActRuntimeAttempt, anaConst.SrcExecutor), 150 ts.DebugMessage("Should have a runtime attempt, events:\n"+suite.summarizeEvents(executorEvents))) 151 suite.Require().Equal(1, countEvents(eventsAfterExecutor, anaConst.CatDebug, anaConst.ActExecutorExit, anaConst.SrcExecutor), 152 ts.DebugMessage("Should have an executor exit event, events:\n"+suite.summarizeEvents(executorEvents))) 153 154 // It's possible due to the timing of the heartbeats and the fact that they are async that we have gotten either 155 // one or two by this point. Technically more is possible, just very unlikely. 156 numHeartbeats := countEvents(executorEvents, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcExecutor) 157 suite.Require().Greater(numHeartbeats, 0, "Should have a heartbeat") 158 suite.Require().LessOrEqual(numHeartbeats, 2, "Should not have excessive heartbeats") 159 var heartbeatEvent *reporters.TestLogEntry 160 for _, e := range executorEvents { 161 if e.Action == anaConst.ActRuntimeHeartbeat { 162 heartbeatEvent = &e 163 } 164 } 165 suite.Require().NotNil(heartbeatEvent, "Should have a heartbeat event") 166 suite.Require().Equal(*heartbeatEvent.Dimensions.ProjectNameSpace, namespace) 167 suite.Require().Equal(*heartbeatEvent.Dimensions.CommitID, commitID) 168 }) 169 170 /* ACTIVATE SHUTDOWN TESTS */ 171 172 cp.SendLine("exit") 173 if runtime.GOOS == "windows" { 174 // We have to exit twice on windows, as we're running through `cmd /k` 175 cp.SendLine("exit") 176 } 177 suite.Require().NoError(rtutils.Timeout(func() error { 178 return cp.ExpectExitCode(0) 179 }, 5*time.Second), ts.DebugMessage("Timed out waiting for exit code")) 180 181 time.Sleep(sleepTime) // give time to let rtwatcher detect process has exited 182 183 // Test that we are no longer sending heartbeats 184 185 events = parseAnalyticsEvents(suite, ts) 186 suite.Require().NotEmpty(events) 187 eventsAfterExit := countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcExecutor) 188 189 time.Sleep(sleepTime) 190 191 eventsAfter := parseAnalyticsEvents(suite, ts) 192 suite.Require().NotEmpty(eventsAfter) 193 eventsAfterExitAndWait := countEvents(eventsAfter, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcExecutor) 194 195 suite.Equal(eventsAfterExit, eventsAfterExitAndWait, 196 fmt.Sprintf("Heartbeats should stop ticking after exiting subshell.\n"+ 197 "Unexpected events: %s", suite.summarizeEvents(filterHeartbeats(eventsAfter[len(events):])), 198 )) 199 200 // Ensure any analytics events from the state tool have the instance ID set 201 for _, e := range events { 202 if strings.Contains(e.Category, "state-svc") || strings.Contains(e.Action, "state-svc") { 203 continue 204 } 205 suite.NotEmpty(e.Dimensions.InstanceID) 206 } 207 208 suite.assertSequentialEvents(events) 209 } 210 211 func (suite *AnalyticsIntegrationTestSuite) TestExecEvents() { 212 suite.OnlyRunForTags(tagsuite.Analytics, tagsuite.Critical) 213 214 /* TEST SETUP */ 215 216 ts := e2e.New(suite.T(), true) 217 defer ts.Close() 218 219 namespace := "ActiveState-CLI/Alternate-Python" 220 commitID := "efcc851f-1451-4d0a-9dcb-074ac3f35f0a" 221 222 // We want to do a clean test without an activate event, so we have to manually seed the yaml 223 ts.PrepareProject(namespace, commitID) 224 225 heartbeatInterval := 1000 // in milliseconds 226 sleepTime := time.Duration(heartbeatInterval) * time.Millisecond 227 sleepTime = sleepTime + (sleepTime / 2) 228 229 env := []string{ 230 constants.DisableRuntime + "=false", 231 fmt.Sprintf("%s=%d", constants.HeartbeatIntervalEnvVarName, heartbeatInterval), 232 } 233 234 /* EXEC TESTS */ 235 236 cp := ts.SpawnWithOpts( 237 e2e.OptArgs("exec", "--", "python3", "-c", fmt.Sprintf("import time; time.sleep(%f); print('DONE')", sleepTime.Seconds())), 238 e2e.OptWD(ts.Dirs.Work), 239 e2e.OptAppendEnv(env...), 240 ) 241 242 cp.Expect("DONE", e2e.RuntimeSourcingTimeoutOpt) 243 244 time.Sleep(sleepTime) 245 246 events := parseAnalyticsEvents(suite, ts) 247 suite.Require().NotEmpty(events) 248 249 runtimeEvents := filterEvents(events, func(e reporters.TestLogEntry) bool { 250 return e.Category == anaConst.CatRuntimeUsage 251 }) 252 253 suite.Equal(1, countEvents(events, anaConst.CatRuntimeUsage, anaConst.ActRuntimeAttempt, anaConst.SrcStateTool), 254 ts.DebugMessage("Should have a runtime attempt, events:\n"+suite.summarizeEvents(runtimeEvents))) 255 256 suite.assertGtEvents(events, 0, anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, anaConst.SrcStateTool, 257 "Expected new heartbeats after state exec") 258 259 cp.ExpectExitCode(0) 260 } 261 262 func countEvents(events []reporters.TestLogEntry, category, action, source string) int { 263 filteredEvents := funk.Filter(events, func(e reporters.TestLogEntry) bool { 264 return e.Category == category && e.Action == action && e.Source == source 265 }).([]reporters.TestLogEntry) 266 return len(filteredEvents) 267 } 268 269 func filterHeartbeats(events []reporters.TestLogEntry) []reporters.TestLogEntry { 270 return filterEvents(events, func(e reporters.TestLogEntry) bool { 271 return e.Category == anaConst.CatRuntimeUsage && e.Action == anaConst.ActRuntimeHeartbeat 272 }) 273 } 274 275 func filterEvents(events []reporters.TestLogEntry, filters ...func(e reporters.TestLogEntry) bool) []reporters.TestLogEntry { 276 filteredEvents := funk.Filter(events, func(e reporters.TestLogEntry) bool { 277 for _, filter := range filters { 278 if !filter(e) { 279 return false 280 } 281 } 282 return true 283 }).([]reporters.TestLogEntry) 284 return filteredEvents 285 } 286 287 func (suite *AnalyticsIntegrationTestSuite) assertNEvents(events []reporters.TestLogEntry, 288 expectedN int, category, action, source string, errMsg string) { 289 suite.Assert().Equal(expectedN, countEvents(events, category, action, source), 290 "Expected %d %s:%s events.\nFile location: %s\nEvents received:\n%s\nError:\n%s", 291 expectedN, category, action, suite.eventsfile, suite.summarizeEvents(events), errMsg) 292 } 293 294 func (suite *AnalyticsIntegrationTestSuite) assertGtEvents(events []reporters.TestLogEntry, 295 greaterThanN int, category, action, source string, errMsg string) { 296 suite.Assert().Greater(countEvents(events, category, action, source), greaterThanN, 297 fmt.Sprintf("Expected more than %d %s:%s events.\nFile location: %s\nEvents received:\n%s\nError:\n%s", 298 greaterThanN, category, action, suite.eventsfile, suite.summarizeEvents(events), errMsg)) 299 } 300 301 func (suite *AnalyticsIntegrationTestSuite) assertSequentialEvents(events []reporters.TestLogEntry) { 302 seq := map[string]int{} 303 304 // Since sequence is established client-side and then reported async it's possible that the sequence does not match the 305 // slice ordering of events 306 sort.Slice(events, func(i, j int) bool { 307 return *events[i].Dimensions.Sequence < *events[j].Dimensions.Sequence 308 }) 309 310 var lastEvent reporters.TestLogEntry 311 for _, ev := range events { 312 if *ev.Dimensions.Sequence == -1 { 313 continue // The sequence of this event is irrelevant 314 } 315 // Sequence is per instance ID 316 key := (*ev.Dimensions.InstanceID)[0:6] 317 if v, ok := seq[key]; ok { 318 if (v + 1) != *ev.Dimensions.Sequence { 319 suite.Failf(fmt.Sprintf("Events are not sequential, expected %d but got %d", v+1, *ev.Dimensions.Sequence), 320 suite.summarizeEventSequence([]reporters.TestLogEntry{ 321 lastEvent, ev, 322 })) 323 } 324 } else { 325 if *ev.Dimensions.Sequence != 0 { 326 suite.Fail(fmt.Sprintf("Sequence should start at 0, got: %v\nevents:\n %v", 327 suite.summarizeEventSequence([]reporters.TestLogEntry{ev}), 328 suite.summarizeEventSequence(events))) 329 } 330 } 331 seq[key] = *ev.Dimensions.Sequence 332 lastEvent = ev 333 } 334 } 335 336 func (suite *AnalyticsIntegrationTestSuite) summarizeEvents(events []reporters.TestLogEntry) string { 337 summary := []string{} 338 for _, event := range events { 339 summary = append(summary, fmt.Sprintf("%s:%s:%s (%s)", event.Category, event.Action, event.Label, event.Source)) 340 } 341 return strings.Join(summary, "\n") 342 } 343 344 func (suite *AnalyticsIntegrationTestSuite) summarizeEventSequence(events []reporters.TestLogEntry) string { 345 summary := []string{} 346 for _, event := range events { 347 summary = append(summary, fmt.Sprintf("%s:%s:%s (%s seq: %s:%s:%d)\n", 348 event.Category, event.Action, event.Label, event.Source, 349 *event.Dimensions.Command, (*event.Dimensions.InstanceID)[0:6], *event.Dimensions.Sequence)) 350 } 351 return strings.Join(summary, "\n") 352 } 353 354 type TestingSuiteForAnalytics interface { 355 Require() *helperSuite.Assertions 356 } 357 358 func parseAnalyticsEvents(suite TestingSuiteForAnalytics, ts *e2e.Session) []reporters.TestLogEntry { 359 time.Sleep(time.Second) // give svc time to process events 360 361 file := filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 362 suite.Require().FileExists(file, ts.DebugMessage("")) 363 364 b, err := fileutils.ReadFile(file) 365 suite.Require().NoError(err) 366 367 var result []reporters.TestLogEntry 368 entries := strings.Split(string(b), "\x00") 369 for _, entry := range entries { 370 if len(entry) == 0 { 371 continue 372 } 373 374 var parsedEntry reporters.TestLogEntry 375 err := json.Unmarshal([]byte(entry), &parsedEntry) 376 suite.Require().NoError(err, fmt.Sprintf("path: %s, value: \n%s\n", file, entry)) 377 result = append(result, parsedEntry) 378 } 379 380 return result 381 } 382 383 func (suite *AnalyticsIntegrationTestSuite) TestShim() { 384 suite.OnlyRunForTags(tagsuite.Analytics) 385 386 ts := e2e.New(suite.T(), true) 387 defer ts.Close() 388 389 asyData := strings.TrimSpace(` 390 project: https://platform.activestate.com/ActiveState-CLI/test 391 scripts: 392 - name: pip 393 language: bash 394 standalone: true 395 value: echo "pip" 396 `) 397 398 ts.PrepareActiveStateYAML(asyData) 399 ts.PrepareCommitIdFile("9090c128-e948-4388-8f7f-96e2c1e00d98") 400 401 cp := ts.SpawnWithOpts( 402 e2e.OptArgs("activate", "ActiveState-CLI/Alternate-Python"), 403 e2e.OptWD(ts.Dirs.Work), 404 ) 405 406 cp.Expect("Creating a Virtual Environment") 407 cp.Expect("Skipping runtime setup") 408 cp.Expect("Activated") 409 cp.ExpectInput(termtest.OptExpectTimeout(10 * time.Second)) 410 411 cp = ts.Spawn("run", "pip") 412 cp.Wait() 413 414 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 415 events := parseAnalyticsEvents(suite, ts) 416 417 var found int 418 for _, event := range events { 419 if event.Category == anaConst.CatRunCmd && event.Action == "run" { 420 found++ 421 suite.Equal(constants.PipShim, event.Label) 422 } 423 } 424 425 if found <= 0 { 426 suite.Fail("Did not find shim event") 427 } 428 } 429 430 func (suite *AnalyticsIntegrationTestSuite) TestSend() { 431 suite.OnlyRunForTags(tagsuite.Analytics, tagsuite.Critical) 432 433 ts := e2e.New(suite.T(), true) 434 defer ts.Close() 435 436 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 437 438 cp := ts.Spawn("--version") 439 cp.Expect("Version") 440 cp.ExpectExitCode(0) 441 442 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 443 444 cp = ts.Spawn("config", "set", constants.ReportAnalyticsConfig, "false") 445 cp.Expect("Successfully") 446 cp.ExpectExitCode(0) 447 448 initialEvents := parseAnalyticsEvents(suite, ts) 449 suite.assertSequentialEvents(initialEvents) 450 451 cp = ts.Spawn("--version") 452 cp.Expect("Version") 453 cp.ExpectExitCode(0) 454 455 events := parseAnalyticsEvents(suite, ts) 456 currentEvents := len(events) 457 if currentEvents > len(initialEvents) { 458 suite.Failf("Should not get additional events", "Got %d additional events, should be 0", currentEvents-len(initialEvents)) 459 } 460 461 suite.assertSequentialEvents(events) 462 } 463 464 func (suite *AnalyticsIntegrationTestSuite) TestSequenceAndFlags() { 465 suite.OnlyRunForTags(tagsuite.Analytics) 466 467 ts := e2e.New(suite.T(), true) 468 defer ts.Close() 469 470 cp := ts.Spawn("--version") 471 cp.Expect("Version") 472 cp.ExpectExitCode(0) 473 474 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 475 events := parseAnalyticsEvents(suite, ts) 476 suite.assertSequentialEvents(events) 477 478 found := false 479 for _, ev := range events { 480 if ev.Category == "run-command" && ev.Action == "" && ev.Label == "--version" { 481 found = true 482 break 483 } 484 } 485 486 suite.True(found, "Should have run-command event with flags, actual: %s", suite.summarizeEvents(events)) 487 } 488 489 func (suite *AnalyticsIntegrationTestSuite) TestInputError() { 490 suite.OnlyRunForTags(tagsuite.Analytics) 491 492 ts := e2e.New(suite.T(), true) 493 defer ts.Close() 494 495 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 496 497 cp := ts.Spawn("clean", "uninstall", "badarg", "--mono") 498 cp.ExpectExitCode(1) 499 ts.IgnoreLogErrors() 500 501 events := parseAnalyticsEvents(suite, ts) 502 suite.assertSequentialEvents(events) 503 504 suite.assertNEvents(events, 1, anaConst.CatDebug, anaConst.ActCommandInputError, anaConst.SrcStateTool, 505 fmt.Sprintf("output:\n%s\n%s", 506 cp.Output(), ts.DebugLogsDump())) 507 508 for _, event := range events { 509 if event.Category == anaConst.CatDebug && event.Action == anaConst.ActCommandInputError { 510 suite.Equal("state clean uninstall --mono", event.Label) 511 } 512 } 513 } 514 515 func (suite *AnalyticsIntegrationTestSuite) TestAttempts() { 516 suite.OnlyRunForTags(tagsuite.Analytics) 517 518 ts := e2e.New(suite.T(), true) 519 defer ts.Close() 520 521 ts.PrepareProject("ActiveState-CLI/test", "9090c128-e948-4388-8f7f-96e2c1e00d98") 522 523 cp := ts.SpawnWithOpts( 524 e2e.OptArgs("activate", "ActiveState-CLI/Alternate-Python"), 525 e2e.OptAppendEnv(constants.DisableRuntime+"=false"), 526 e2e.OptAppendEnv(constants.DisableActivateEventsEnvVarName+"=false"), 527 e2e.OptWD(ts.Dirs.Work), 528 ) 529 530 cp.Expect("Creating a Virtual Environment") 531 cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) 532 cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) 533 534 cp.SendLine("python3 --version") 535 cp.Expect("Python 3.") 536 537 time.Sleep(time.Second) // Ensure state-svc has time to report events 538 539 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 540 events := parseAnalyticsEvents(suite, ts) 541 542 var foundAttempts int 543 var foundExecs int 544 for _, e := range events { 545 if strings.Contains(e.Category, "runtime") && strings.Contains(e.Action, "attempt") { 546 foundAttempts++ 547 if strings.Contains(*e.Dimensions.Trigger, "exec") && strings.Contains(e.Source, anaConst.SrcExecutor) { 548 foundExecs++ 549 } 550 } 551 } 552 553 if foundAttempts == 2 { 554 suite.Fail("Should find multiple runtime attempts") 555 } 556 if foundExecs == 1 { 557 suite.Fail("Should find one exec event") 558 } 559 } 560 561 func (suite *AnalyticsIntegrationTestSuite) TestHeapEvents() { 562 suite.OnlyRunForTags(tagsuite.Analytics) 563 564 ts := e2e.New(suite.T(), true) 565 defer ts.Close() 566 567 ts.LoginAsPersistentUser() 568 569 cp := ts.SpawnWithOpts(e2e.OptArgs("activate", "ActiveState-CLI/Alternate-Python"), 570 e2e.OptWD(ts.Dirs.Work), 571 ) 572 573 cp.Expect("Creating a Virtual Environment") 574 cp.Expect("Activated", e2e.RuntimeSourcingTimeoutOpt) 575 cp.ExpectInput(termtest.OptExpectTimeout(120 * time.Second)) 576 577 time.Sleep(time.Second) // Ensure state-svc has time to report events 578 579 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 580 581 events := parseAnalyticsEvents(suite, ts) 582 suite.Require().NotEmpty(events) 583 584 // Ensure analytics events have required/important fields 585 for _, e := range events { 586 // Skip events that are not relevant to Heap 587 // State Service, Update, and Auth events can run before a user has logged in 588 if strings.Contains(e.Category, "state-svc") || strings.Contains(e.Action, "state-svc") || strings.Contains(e.Action, "auth") || strings.Contains(e.Category, "update") { 589 continue 590 } 591 592 // UserID is used to identify the user 593 suite.NotEmpty(e.Dimensions.UserID, "Event should have a user ID") 594 595 // Category and Action are primary attributes reported to Heap and should be set 596 suite.NotEmpty(e.Category, "Event category should not be empty") 597 suite.NotEmpty(e.Action, "Event action should not be empty") 598 } 599 600 suite.assertSequentialEvents(events) 601 } 602 603 func (suite *AnalyticsIntegrationTestSuite) TestConfigEvents() { 604 suite.OnlyRunForTags(tagsuite.Analytics, tagsuite.Config) 605 606 ts := e2e.New(suite.T(), true) 607 defer ts.Close() 608 609 cp := ts.SpawnWithOpts(e2e.OptArgs("config", "set", "optin.unstable", "false"), 610 e2e.OptWD(ts.Dirs.Work), 611 ) 612 cp.Expect("Successfully set config key") 613 614 time.Sleep(time.Second) // Ensure state-svc has time to report events 615 616 cp = ts.SpawnWithOpts(e2e.OptArgs("config", "set", "optin.unstable", "true"), 617 e2e.OptWD(ts.Dirs.Work), 618 ) 619 cp.Expect("Successfully set config key") 620 621 time.Sleep(time.Second) // Ensure state-svc has time to report events 622 623 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 624 625 events := parseAnalyticsEvents(suite, ts) 626 suite.Require().NotEmpty(events) 627 628 // Ensure analytics events have required/important fields 629 var found int 630 for _, e := range events { 631 if !strings.Contains(e.Category, anaConst.CatConfig) { 632 continue 633 } 634 635 if e.Label != "optin.unstable" { 636 suite.Fail("Incorrect config event label") 637 } 638 found++ 639 } 640 641 if found < 2 { 642 suite.Fail("Should find multiple config events") 643 } 644 645 suite.assertNEvents(events, 1, anaConst.CatConfig, anaConst.ActConfigSet, anaConst.SrcStateTool, "Should be at one config set event") 646 suite.assertNEvents(events, 1, anaConst.CatConfig, anaConst.ActConfigUnset, anaConst.SrcStateTool, "Should be at one config unset event") 647 suite.assertSequentialEvents(events) 648 } 649 650 func (suite *AnalyticsIntegrationTestSuite) TestCIAndInteractiveDimensions() { 651 suite.OnlyRunForTags(tagsuite.Analytics) 652 653 ts := e2e.New(suite.T(), true) 654 defer ts.Close() 655 656 for _, interactive := range []bool{true, false} { 657 suite.T().Run(fmt.Sprintf("interactive: %v", interactive), func(t *testing.T) { 658 args := []string{"--version"} 659 if !interactive { 660 args = append(args, "--non-interactive") 661 } 662 cp := ts.SpawnWithOpts(e2e.OptArgs(args...)) 663 cp.Expect("ActiveState CLI") 664 cp.ExpectExitCode(0) 665 666 time.Sleep(time.Second) // Ensure state-svc has time to report events 667 668 suite.eventsfile = filepath.Join(ts.Dirs.Config, reporters.TestReportFilename) 669 events := parseAnalyticsEvents(suite, ts) 670 suite.Require().NotEmpty(events) 671 processedAnEvent := false 672 for _, e := range events { 673 if !strings.Contains(e.Category, anaConst.CatRunCmd) || e.Label == "" { 674 continue // only look at spawned run-command events 675 } 676 interactiveEvent := !strings.Contains(e.Label, "--non-interactive") 677 if interactive != interactiveEvent { 678 continue // ignore the other spawned command 679 } 680 suite.Equal(condition.OnCI(), *e.Dimensions.CI, "analytics should report being on CI") 681 suite.Equal(interactive, *e.Dimensions.Interactive, "analytics did not report the correct interactive value for %v", e) 682 suite.Equal(condition.OnCI(), // not InActiveStateCI() because if it's false, we forgot to set ACTIVESTATE_CI env var in GitHub Actions scripts 683 *e.Dimensions.ActiveStateCI, "analytics did not report being in ActiveState CI") 684 processedAnEvent = true 685 } 686 suite.True(processedAnEvent, "did not actually test CI and Interactive dimensions") 687 suite.assertSequentialEvents(events) 688 }) 689 } 690 } 691 692 func TestAnalyticsIntegrationTestSuite(t *testing.T) { 693 suite.Run(t, new(AnalyticsIntegrationTestSuite)) 694 }