Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add analytics event processing [IDE-736] #712

Merged
merged 32 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
10a093b
chore: separate lint from unit tests
bastiandoetsch Oct 28, 2024
aca0e71
fix: syntax error in build.yaml
bastiandoetsch Oct 28, 2024
47f338e
fix: another syntax error
bastiandoetsch Oct 28, 2024
d5daf71
chore: make all build jobs depend on lint job
bastiandoetsch Oct 28, 2024
48684b4
feat: enable sending of event data with helper struct
bastiandoetsch Oct 28, 2024
a48a29b
chore: remove automatic release on commit
bastiandoetsch Oct 28, 2024
639133f
feat: add auth analytics
bastiandoetsch Oct 28, 2024
d7d34bc
fix: add pact test for analytics event flow
bastiandoetsch Oct 29, 2024
357252f
fix: add error to authentication event reporting
bastiandoetsch Oct 29, 2024
fa2588c
fix: add early returns
bastiandoetsch Oct 29, 2024
d765649
feat: add auth event
bastiandoetsch Oct 29, 2024
9525306
chore: remove warning
bastiandoetsch Oct 29, 2024
fca7e30
Merge branch 'main' into feat/IDE-736_add-analytics-event
bastiandoetsch Oct 29, 2024
0cee479
refactor: remove duplication
bastiandoetsch Oct 29, 2024
c0e0e4a
fix: temporary fix for failing test
bastiandoetsch Oct 29, 2024
ca52030
fix: tests
bastiandoetsch Oct 29, 2024
bff57b0
fix: update github release workflow
bastiandoetsch Oct 29, 2024
fa61de7
fix: parallelization test
bastiandoetsch Oct 29, 2024
3f29c86
fix: test
bastiandoetsch Oct 29, 2024
1eb564a
fix: cli installation in tests
bastiandoetsch Oct 29, 2024
fd54e35
fix: make cli installation independent between tests
bastiandoetsch Oct 29, 2024
1fe6fbc
fix: cli path issues for concurrent tests
bastiandoetsch Oct 29, 2024
cdd697e
fix: linting problem
bastiandoetsch Oct 29, 2024
ff1c8d6
fix: use lock file relative to cli path
bastiandoetsch Oct 29, 2024
8838d4c
fix: lock file for download - use parent dir, not cli file
bastiandoetsch Oct 29, 2024
3157acf
fix: lock file creation
bastiandoetsch Oct 29, 2024
69ae2d3
fix: linter
bastiandoetsch Oct 29, 2024
4b6e1b0
fix: race condition in tests
bastiandoetsch Oct 30, 2024
ba44271
fix: tests
bastiandoetsch Oct 30, 2024
6d625c0
chore: enable trace logging for failing test
bastiandoetsch Oct 30, 2024
e720e1f
chore: add more debug information for failing test
bastiandoetsch Oct 30, 2024
5e2ebc4
chore: disable Test_SmokeIssueCaching on windows
bastiandoetsch Oct 30, 2024
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
33 changes: 28 additions & 5 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,28 @@ on:
pull_request:

jobs:
lint:
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
name: lint
runs-on: ubuntu-latest
steps:
- name: Prepare git
run: git config --global core.autocrlf false

- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "./go.mod"
cache: "true"

- name: Lint source code
run: |
make tools lint

unit-tests:
name: unit tests
needs: [lint]
runs-on: ubuntu-latest
steps:
- name: Prepare git
Expand Down Expand Up @@ -49,10 +69,6 @@ jobs:
run: |
make tools

- name: Lint source code
run: |
make tools lint

- name: Run tests
env:
DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}}
Expand All @@ -70,6 +86,7 @@ jobs:

integration-tests:
name: integration-tests
needs: [lint]
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand Down Expand Up @@ -99,12 +116,13 @@ jobs:
run: |
make tools

- name: Run integration tests with Pact
- name: Run integration & smoke tests with Pact
if: matrix.os == 'ubuntu-latest'
env:
DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}}
SNYK_TOKEN: ${{secrets.SNYK_TOKEN }}
INTEG_TESTS: "true"
SMOKE_TESTS: "true"
bastiandoetsch marked this conversation as resolved.
Show resolved Hide resolved
run: |
export PATH=$PATH:~/pact/bin

Expand All @@ -121,6 +139,7 @@ jobs:
DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}}
SNYK_TOKEN: ${{secrets.SNYK_TOKEN }}
INTEG_TESTS: "true"
SMOKE_TESTS: "true"
run: |
export PATH=$PATH:~/pact/bin

Expand All @@ -136,12 +155,14 @@ jobs:
DEEPROXY_API_URL: ${{secrets.DEEPROXY_API_URL}}
SNYK_TOKEN: ${{secrets.SNYK_TOKEN }}
INTEG_TESTS: "true"
SMOKE_TESTS: "true"
run: |
make clean test


proxy-test:
name: proxy-test
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -154,6 +175,7 @@ jobs:

race-tests:
name: race-test
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -216,6 +238,7 @@ jobs:
push: true
test-release:
name: test-release
needs: [lint, unit-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- "main"
bastiandoetsch marked this conversation as resolved.
Show resolved Hide resolved

# Grant an OIDC token which we can exchange for an AWS IAM role
permissions:
Expand Down
50 changes: 49 additions & 1 deletion domain/ide/command/report_analytics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package command

import (
"context"
"encoding/json"
"testing"

"github.com/golang/mock/gomock"

localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
"github.com/snyk/go-application-framework/pkg/workflow"
"github.com/stretchr/testify/mock"

"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/infrastructure/authentication"
Expand Down Expand Up @@ -51,6 +53,52 @@ func Test_ReportAnalyticsCommand_IsCallingExtension(t *testing.T) {
require.Emptyf(t, output, "output should be empty")
}

func Test_ReportAnalyticsCommand_PlugInstalledEvent(t *testing.T) {
c := testutil.UnitTest(t)

testInput := types.AnalyticsEventParam{
InteractionType: "plugin installed",
Category: []string{"install"},
Status: "success",
TargetId: "pkg:file/none",
TimestampMs: 123,
Extension: map[string]any{"device_id": c.DeviceID()},
}

marshal, err := json.Marshal(testInput)
if err != nil {
return
}

cmd := setupReportAnalyticsCommand(t, c, string(marshal))

mockEngine, engineConfig := setUpEngineMock(t, c)
mockEngine.EXPECT().GetConfiguration().Return(engineConfig).AnyTimes()

mockEngine.EXPECT().InvokeWithInputAndConfig(
localworkflows.WORKFLOWID_REPORT_ANALYTICS,
mock.MatchedBy(func(i interface{}) bool {
inputData, ok := i.([]workflow.Data)
require.Truef(t, ok, "input should be workflow data")
require.Lenf(t, inputData, 1, "should only have one input")

payload := string(inputData[0].GetPayload().([]byte))

require.Contains(t, payload, "plugin installed")
require.Contains(t, payload, "install")
require.Contains(t, payload, "device_id")
require.Contains(t, payload, "123")

return true
}),
gomock.Any(),
).Return(nil, nil)

output, err := cmd.Execute(context.Background())
require.NoError(t, err)
require.Emptyf(t, output, "output should be empty")
}

func setupReportAnalyticsCommand(t *testing.T, c *config.Config, testInput string) *reportAnalyticsCommand {
t.Helper()
provider := authentication.NewFakeCliAuthenticationProvider(c)
Expand Down
46 changes: 17 additions & 29 deletions domain/ide/workspace/folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ import (

"github.com/snyk/snyk-ls/internal/delta"

"github.com/snyk/snyk-ls/internal/types"
"github.com/snyk/snyk-ls/internal/util"

"github.com/google/uuid"
"github.com/puzpuzpuz/xsync/v3"

"github.com/snyk/snyk-ls/internal/types"

gafanalytics "github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/instrumentation"
"github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas"
Expand Down Expand Up @@ -376,7 +374,6 @@ func (f *Folder) updateGlobalCacheAndSeverityCounts(scanData *snyk.ScanData) {

func sendAnalytics(data *snyk.ScanData) {
c := config.CurrentConfig()
gafConfig := c.Engine().GetConfiguration()

logger := c.Logger().With().Str("method", "folder.sendAnalytics").Logger()
if data.Product == "" {
Expand All @@ -389,38 +386,29 @@ func sendAnalytics(data *snyk.ScanData) {
return
}

ic := gafanalytics.NewInstrumentationCollector()

//Add to the interaction attribute in the analytics event
iid := instrumentation.AssembleUrnFromUUID(uuid.NewString())
ic.SetInteractionId(iid)
ic.SetTimestamp(data.TimestampFinished)
ic.SetStage("dev")
ic.SetInteractionType("Scan done")

// this information is not filled automatically, so we need to collect it
categories := setupCategories(data, c)
ic.SetCategory(categories)
ic.SetStatus(gafanalytics.Success)

summary := createTestSummary(data, c)
ic.SetTestSummary(summary)

targetId, err := instrumentation.GetTargetId(data.Path, instrumentation.AutoDetectedTargetId)
if err != nil {
logger.Err(err).Msg("Error creating the Target Id")
}
ic.SetTargetId(targetId)
summary := createTestSummary(data, c)

ic.AddExtension("device_id", c.DeviceID())
ic.AddExtension("is_delta_scan", data.IsDeltaScan)
param := types.AnalyticsEventParam{
InteractionType: "Scan done",
Category: categories,
Status: string(gafanalytics.Success),
TargetId: targetId,
TimestampMs: data.TimestampFinished.UnixMilli(),
DurationMs: int64(data.DurationMs),
Extension: map[string]any{"is_delta_scan": data.IsDeltaScan},
}

//Populate the runtime attribute of the analytics event
ua := util.GetUserAgent(gafConfig, config.Version)
ic.SetUserAgent(ua)
ic.SetDuration(data.DurationMs)
ic := analytics.PayloadForAnalyticsEventParam(c, param)

//Set the final type attribute of the analytics event
ic.SetType("analytics")
// test specific data is not handled in the PayloadForAnalytics helper
// and must be added explicitly
ic.SetTestSummary(summary)

analyticsData, err := gafanalytics.GetV2InstrumentationObject(ic)
if err != nil {
Expand Down
68 changes: 61 additions & 7 deletions infrastructure/analytics/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,62 @@
package analytics

import (
"encoding/json"
"sync"
"time"

"github.com/google/uuid"
"github.com/snyk/go-application-framework/pkg/analytics"
configuration2 "github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/instrumentation"
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
"github.com/snyk/go-application-framework/pkg/workflow"

"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/internal/types"
"github.com/snyk/snyk-ls/internal/util"
)

var analyticsMu = sync.RWMutex{}

func SendAnalyticsToAPI(c *config.Config, payload []byte) error {
logger := c.Logger().With().Str("method", "analytics.sendAnalyticsToAPI").Logger()
logger.Debug().Str("payload", string(payload)).Msg("Analytics Payload")

inputData := workflow.NewData(
workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"),
"application/json",
payload,
)
var eventsParam types.AnalyticsEventParam
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tries to get a struct - unmarshalling into a struct does not work, we use the old flow, passing the data through to analytics service.

err := json.Unmarshal(payload, &eventsParam)
var inputData workflow.Data
if err == nil {
ic := PayloadForAnalyticsEventParam(c, eventsParam)
instrumentationObject, icErr := analytics.GetV2InstrumentationObject(ic)
if icErr != nil {
return err
}

bytes, marshalErr := json.Marshal(instrumentationObject)
if marshalErr != nil {
return marshalErr
}

logger.Debug().Str("payload", string(bytes)).Msg("Analytics Payload")
inputData = workflow.NewData(
workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"),
"application/json",
bytes,
)
} else {
logger.Debug().Str("payload", string(payload)).Msg("Analytics Payload")
inputData = workflow.NewData(
workflow.NewTypeIdentifier(localworkflows.WORKFLOWID_REPORT_ANALYTICS, "reportAnalytics"),
"application/json",
payload,
)
}

engine := c.Engine()
configuration := engine.GetConfiguration().Clone()
configuration.Set(configuration2.FLAG_EXPERIMENTAL, true)
analyticsMu.Lock()
_, err := engine.InvokeWithInputAndConfig(
_, err = engine.InvokeWithInputAndConfig(
localworkflows.WORKFLOWID_REPORT_ANALYTICS,
[]workflow.Data{inputData},
configuration,
Expand All @@ -55,3 +85,27 @@ func SendAnalyticsToAPI(c *config.Config, payload []byte) error {
}
return nil
}

func PayloadForAnalyticsEventParam(c *config.Config, param types.AnalyticsEventParam) analytics.InstrumentationCollector {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bundles filling the most common event fields.

ic := analytics.NewInstrumentationCollector()
//Add to the interaction attribute in the analytics event
iid := instrumentation.AssembleUrnFromUUID(uuid.NewString())

//Set the final type attribute of the analytics event
ic.SetType("analytics")
ic.SetInteractionId(iid)
ic.SetStage("dev")
ic.AddExtension("device_id", c.DeviceID())
for s, a := range param.Extension {
ic.AddExtension(s, a)
}
ua := util.GetUserAgent(c.Engine().GetConfiguration(), config.Version)
ic.SetUserAgent(ua)

ic.SetTimestamp(time.UnixMilli(param.TimestampMs))
ic.SetInteractionType(param.InteractionType)
ic.SetStatus(analytics.Status(param.Status))
ic.SetCategory(param.Category)
ic.SetTargetId(param.TargetId)
return ic
}
12 changes: 12 additions & 0 deletions internal/types/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,15 @@ func (service *CommandServiceMock) ExecutedCommands() []CommandData {
service.m.Unlock()
return cmds
}

type AnalyticsEventParam struct {
InteractionType string `json:"interactionType"`
Category []string `json:"category"`
Status string `json:"status"`
TargetId string `json:"targetId"`
TimestampMs int64 `json:"timestampMs"`
DurationMs int64 `json:"durationMs"`
Results map[string]any `json:"results"`
Errors []any `json:"errors"`
Extension map[string]any `json:"extension"`
}
Loading