Skip to content

Commit

Permalink
feat: add scan source to metrics [IDE-924] (#781)
Browse files Browse the repository at this point in the history
Co-authored-by: bastiandoetsch <[email protected]>
  • Loading branch information
bastiandoetsch and bastiandoetsch authored Feb 24, 2025
1 parent 0f03ea6 commit 881c27c
Show file tree
Hide file tree
Showing 24 changed files with 396 additions and 109 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ Right now the language server supports the following actions:
- params: `types.PublishDiagnosticsParams`
- note: alias for textDocument/publishDiagnostics


- MCP Server URL Notification to publish the listening address. The server listens for `POST` requests on `/messages` and for SSE subscriptions on `/sse`. An example can be found in the mcp package in the smoke test.
- method: `$/snyk.mcpServerURL`
- params: `types.McpServerURLParams`
- example:
```json5
{
"url": "https://127.0.0.1:7595"
}
```

- Authentication Notification
- method: `$/snyk.hasAuthenticated`
- params: `types.AuthenticationParams`
Expand Down
35 changes: 31 additions & 4 deletions application/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,8 @@ import (

"github.com/adrg/xdg"
"github.com/denisbrodbeck/machineid"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/xtgo/uuid"
"golang.org/x/oauth2"

"github.com/snyk/go-application-framework/pkg/app"
"github.com/snyk/go-application-framework/pkg/auth"
"github.com/snyk/go-application-framework/pkg/configuration"
Expand All @@ -46,6 +44,8 @@ import (
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
"github.com/snyk/go-application-framework/pkg/workflow"

"golang.org/x/oauth2"

"github.com/snyk/snyk-ls/infrastructure/cli/cli_constants"
"github.com/snyk/snyk-ls/infrastructure/cli/filename"
"github.com/snyk/snyk-ls/internal/logging"
Expand Down Expand Up @@ -200,6 +200,8 @@ type Config struct {
offline bool
ws types.Workspace
mcpServerEnabled bool
mcpBaseURL *url.URL
isLSPInitialized bool
}

func CurrentConfig() *Config {
Expand Down Expand Up @@ -329,7 +331,7 @@ func (c *Config) determineDeviceId() string {
if c.token != "" {
return util.Hash([]byte(c.token))
} else {
return uuid.NewTime().String()
return uuid.NewString()
}
} else {
return id
Expand Down Expand Up @@ -1221,3 +1223,28 @@ func (c *Config) McpServerEnabled() bool {

return c.mcpServerEnabled
}

func (c *Config) SetMCPServerURL(baseURL *url.URL) {
c.m.Lock()
defer c.m.Unlock()
c.mcpBaseURL = baseURL
}

func (c *Config) GetMCPServerURL() *url.URL {
c.m.RLock()
defer c.m.RUnlock()

return c.mcpBaseURL
}

func (c *Config) IsLSPInitialized() bool {
c.m.RLock()
defer c.m.RUnlock()
return c.isLSPInitialized
}

func (c *Config) SetLSPInitialized(initialized bool) {
c.m.Lock()
defer c.m.Unlock()
c.isLSPInitialized = initialized
}
16 changes: 11 additions & 5 deletions application/server/execute_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,25 @@ func Test_executeWorkspaceScanCommand_shouldAskForTrust(t *testing.T) {
func Test_loginCommand_StartsAuthentication(t *testing.T) {
c := testutil.UnitTest(t)
loc, jsonRPCRecorder := setupServer(t, c)
c.SetAutomaticAuthentication(false)
c.SetAuthenticationMethod(types.FakeAuthentication)

authenticationService := di.AuthenticationService()
fakeAuthenticationProvider := authenticationService.Provider().(*authentication.FakeAuthenticationProvider)
fakeAuthenticationProvider.IsAuthenticated = false

// reset to use real service
command.SetService(command.NewService(di.AuthenticationService(), nil, nil, nil, nil, nil, nil))
command.SetService(command.NewService(authenticationService, di.Notifier(), di.LearnService(), nil, nil, nil, nil))

config.CurrentConfig().SetAutomaticAuthentication(false)
_, err := loc.Client.Call(ctx, "initialize", nil)
if err != nil {
t.Fatal(err)
}
fakeAuthenticationProvider := di.AuthenticationService().Provider().(*authentication.FakeAuthenticationProvider)
fakeAuthenticationProvider.IsAuthenticated = false
params := lsp.ExecuteCommandParams{Command: types.LoginCommand}

_, err = loc.Client.Call(ctx, "initialized", types.InitializedParams{})
assert.NoError(t, err)

// Act
tokenResponse, err := loc.Client.Call(ctx, "workspace/executeCommand", params)
if err != nil {
Expand All @@ -147,7 +153,7 @@ func Test_loginCommand_StartsAuthentication(t *testing.T) {
// Assert
assert.NotEmpty(t, tokenResponse.ResultString())
assert.True(t, fakeAuthenticationProvider.IsAuthenticated)
assert.Eventually(t, func() bool { return len(jsonRPCRecorder.Notifications()) > 0 }, 5*time.Second, 50*time.Millisecond)
assert.Eventually(t, func() bool { return len(jsonRPCRecorder.Notifications()) > 0 }, 10*time.Second, 50*time.Millisecond)
notifications := jsonRPCRecorder.FindNotificationsByMethod("$/snyk.hasAuthenticated")
assert.Equal(t, 1, len(notifications))
var hasAuthenticatedNotification types.AuthenticationParams
Expand Down
12 changes: 10 additions & 2 deletions application/server/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@ func disposeProgressListener() {
progressStopChan <- true
}

//nolint:gocyclo // this is ok, as it's so high because of forwarding the calls
func registerNotifier(c *config.Config, srv types.Server) {
logger := c.Logger().With().Str("method", "registerNotifier").Logger()
callbackFunction := func(params any) {
for !c.IsLSPInitialized() {
logger.Debug().Msg("waiting for lsp initialization to be finished...")
time.Sleep(300 * time.Millisecond)
}
switch params := params.(type) {
case types.GetSdk:
handleGetSdks(params, logger, srv)
Expand Down Expand Up @@ -140,6 +145,9 @@ func registerNotifier(c *config.Config, srv types.Server) {
handleInlineValueRefresh(srv, &logger)
logger.Debug().
Msg("sending inline value refresh request to client")
case types.McpServerURLParams:
logger.Debug().Msgf("sending mcp url %s", params.URL)
notifier(c, srv, "$/snyk.mcpServerURL", params)
default:
logger.Warn().
Interface("params", params).
Expand All @@ -165,14 +173,14 @@ func handleGetSdks(params types.GetSdk, logger zerolog.Logger, srv types.Server)

callback, err := srv.Callback(ctx, "workspace/snyk.sdks", folder)
if err != nil {
logger.Warn().Err(err).Str("folderPath", params.FolderPath).Msg("could not retrieve sdk")
logger.Debug().Str("folderPath", params.FolderPath).Msg("could not retrieve sdk, most likely not yet supported by IDE, continuing...")
return
}

// unmarshall into array that is transferred back via the channel on exit
err = callback.UnmarshalResult(&sdks)
if err != nil {
logger.Warn().Err(err).Str("resultString", callback.ResultString()).Msg("could not unmarshal sdk response")
logger.Debug().Str("resultString", callback.ResultString()).Msg("could not get sdk response, most likely not yet supported by IDE, continuing...")
return
}
}
Expand Down
8 changes: 5 additions & 3 deletions application/server/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ func Test_NotifierShouldSendNotificationToClient(t *testing.T) {
}
var expected = types.AuthenticationParams{Token: "test token", ApiUrl: "https://api.snyk.io"}

c.SetLSPInitialized(true)

di.Notifier().Send(expected)
assert.Eventually(
t,
Expand Down Expand Up @@ -180,7 +182,7 @@ func Test_IsAvailableCliNotification(t *testing.T) {
t.Fatal(err)
}
var expected = types.SnykIsAvailableCli{CliPath: filepath.Join(t.TempDir(), "cli")}

c.SetLSPInitialized(true)
di.Notifier().Send(expected)
assert.Eventually(
t,
Expand Down Expand Up @@ -212,7 +214,7 @@ func TestShowMessageRequest(t *testing.T) {
if err != nil {
t.Fatal(err)
}

c.SetLSPInitialized(true)
actionCommandMap := data_structure.NewOrderedMap[types.MessageAction, types.CommandData]()
expectedTitle := "test title"
// data, err := command.CreateFromCommandData(snyk.CommandData{
Expand Down Expand Up @@ -270,7 +272,7 @@ func TestShowMessageRequest(t *testing.T) {
actionCommandMap.Add(types.MessageAction(selectedAction), types.CommandData{CommandId: types.OpenBrowserCommand, Arguments: []any{"https://snyk.io"}})

request := types.ShowMessageRequest{Message: "message", Type: types.Info, Actions: actionCommandMap}

c.SetLSPInitialized(true)
di.Notifier().Send(request)

assert.Eventually(
Expand Down
12 changes: 9 additions & 3 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func Start(c *config.Config) {
logger.Info().Msg("Starting up MCP Server...")
var mcpServer *mcp2.McpLLMBinding
go func() {
mcpServer = mcp2.NewMcpServer(c, mcp2.WithScanner(di.Scanner()), mcp2.WithLogger(c.Logger()))
mcpServer = mcp2.NewMcpLLMBinding(c, mcp2.WithScanner(di.Scanner()), mcp2.WithLogger(c.Logger()))
err := mcpServer.Start()
if err != nil {
c.Logger().Err(err).Msg("failed to start mcp server")
Expand Down Expand Up @@ -408,6 +408,10 @@ func initializedHandler(srv *jrpc2.Server) handler.Func {
// looks weird when including the method name.
c := config.CurrentConfig()
initialLogger := c.Logger()
// only set our config to initialized after leaving the func
defer func() {
c.SetLSPInitialized(true)
}()
initialLogger.Info().Msg("snyk-ls: " + config.Version + " (" + util.Result(os.Executable()) + ")")
initialLogger.Info().Msgf("CLI Path: %s", c.CliSettings().Path())
initialLogger.Info().Msgf("CLI Installed? %t", c.CliSettings().Installed())
Expand Down Expand Up @@ -460,8 +464,10 @@ func initializedHandler(srv *jrpc2.Server) handler.Func {
)
logger.Info().Msg(msg)
}

logger.Debug().Msg("trying to get trusted status for untrusted folders")
mcpServerURL := c.GetMCPServerURL()
if mcpServerURL != nil {
di.Notifier().Send(types.McpServerURLParams{URL: mcpServerURL.String()})
}
return nil, nil
})
}
Expand Down
2 changes: 2 additions & 0 deletions application/server/server_smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,8 @@ func Test_SmokeSnykCodeFileScan(t *testing.T) {
f := workspace.NewFolder(c, cloneTargetDir, "Test", di.Scanner(), di.HoverService(), di.ScanNotifier(), di.Notifier(), di.ScanPersister(), di.ScanStateAggregator())
w.AddFolder(f)

c.SetLSPInitialized(true)

_ = textDocumentDidSave(t, &loc, testPath)

assert.Eventually(t, checkForPublishedDiagnostics(t, c, testPath, -1, jsonRPCRecorder), 2*time.Minute, 10*time.Millisecond)
Expand Down
45 changes: 45 additions & 0 deletions application/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package server

import (
"context"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -238,6 +239,44 @@ func Test_initialized_shouldCheckRequiredProtocolVersion(t *testing.T) {
"did not receive callback because of wrong protocol version")
}

func Test_initialized_shouldSendMcpServerAddress(t *testing.T) {
c := testutil.UnitTest(t)
loc, jsonRpcRecorder := setupServer(t, c)

params := types.InitializeParams{
InitializationOptions: types.Settings{RequiredProtocolVersion: config.LsProtocolVersion},
}

rsp, err := loc.Client.Call(ctx, "initialize", params)
require.NoError(t, err)
var result types.InitializeResult
err = rsp.UnmarshalResult(&result)
require.NoError(t, err)

testURL, err := url.Parse("http://localhost:1234")
require.NoError(t, err)

c.SetMCPServerURL(testURL)

_, err = loc.Client.Call(ctx, "initialized", params)
require.NoError(t, err)
require.Eventuallyf(t, func() bool {
n := jsonRpcRecorder.FindNotificationsByMethod("$/snyk.mcpServerURL")
if n == nil {
return false
}
if len(n) > 1 {
t.Fatal("can't succeed anymore, too many notifications ", n)
}

var param types.McpServerURLParams
err = n[0].UnmarshalParams(&param)
require.NoError(t, err)
return param.URL == testURL.String()
}, time.Minute, time.Millisecond,
"did not receive mcp server url")
}

func Test_initialize_shouldSupportAllCommands(t *testing.T) {
c := testutil.UnitTest(t)
loc, _ := setupServer(t, c)
Expand Down Expand Up @@ -750,6 +789,8 @@ func Test_textDocumentDidSaveHandler_shouldAcceptDocumentItemAndPublishDiagnosti
t.Fatal(err)
}

c.SetLSPInitialized(true)

filePath, fileDir := code.TempWorkdirWithIssues(t)
fileUri := sendFileSavedMessage(t, filePath, fileDir, loc)

Expand Down Expand Up @@ -802,6 +843,8 @@ func Test_textDocumentDidSaveHandler_shouldTriggerScanForDotSnykFile(t *testing.
t.Fatalf("initialization failed: %v", err)
}

c.SetLSPInitialized(true)

snykFilePath, folderPath := createTemporaryDirectoryWithSnykFile(t)

sendFileSavedMessage(t, snykFilePath, folderPath, loc)
Expand Down Expand Up @@ -854,6 +897,8 @@ func Test_textDocumentDidOpenHandler_shouldPublishIfCached(t *testing.T) {
t.Fatal(err)
}

c.SetLSPInitialized(true)

filePath, fileDir := code.TempWorkdirWithIssues(t)
fileUri := sendFileSavedMessage(t, filePath, fileDir, loc)

Expand Down
1 change: 1 addition & 0 deletions application/server/trust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func Test_handleUntrustedFolders_shouldTriggerTrustRequestAndScanAfterConfirmati
w := c.Workspace()
sc := &scanner.TestScanner{}
c.SetTrustedFolderFeatureEnabled(true)
c.SetLSPInitialized(true)
w.AddFolder(workspace.NewFolder(c, "/trusted/dummy", "dummy", sc, di.HoverService(), di.ScanNotifier(), di.Notifier(), di.ScanPersister(), di.ScanStateAggregator()))

command.HandleUntrustedFolders(context.Background(), c, loc.Server)
Expand Down
8 changes: 4 additions & 4 deletions domain/ide/command/command_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func Service() types.CommandService {
return instance
}

func (service *serviceImpl) ExecuteCommandData(ctx context.Context, commandData types.CommandData, server types.Server) (any, error) {
func (s *serviceImpl) ExecuteCommandData(ctx context.Context, commandData types.CommandData, server types.Server) (any, error) {
c := config.CurrentConfig()
logger := c.Logger().With().Str("method", "command.serviceImpl.ExecuteCommandData").Logger()
if c.Offline() {
Expand All @@ -77,7 +77,7 @@ func (service *serviceImpl) ExecuteCommandData(ctx context.Context, commandData

logger.Debug().Msgf("executing command %s", commandData.CommandId)

command, err := CreateFromCommandData(c, commandData, server, service.authService, service.learnService, service.notifier, service.issueProvider, service.codeApiClient, service.codeScanner, service.cli)
command, err := CreateFromCommandData(c, commandData, server, s.authService, s.learnService, s.notifier, s.issueProvider, s.codeApiClient, s.codeScanner, s.cli)
if err != nil {
logger.Err(err).Msg("failed to create command")
return nil, err
Expand All @@ -90,8 +90,8 @@ func (service *serviceImpl) ExecuteCommandData(ctx context.Context, commandData
}

if err != nil && strings.Contains(err.Error(), "400 Bad Request") {
service.notifier.SendShowMessage(sglsp.MTWarning, "Logging out automatically, available credentials are invalid. Please re-authenticate.")
service.authService.Logout(ctx)
s.notifier.SendShowMessage(sglsp.MTWarning, "Logging out automatically, available credentials are invalid. Please re-authenticate.")
s.authService.Logout(ctx)
return nil, nil
}

Expand Down
Loading

0 comments on commit 881c27c

Please sign in to comment.