Skip to content
Draft
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.14.0
google.golang.org/protobuf v1.34.2
howett.net/plist v1.0.1
)

require (
Expand All @@ -21,5 +22,4 @@ require (
golang.org/x/sys v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
Expand Down
21 changes: 21 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/alecthomas/kong"

"github.com/bugsnag/bugsnag-cli/pkg/build"
"github.com/bugsnag/bugsnag-cli/pkg/data_access"
"github.com/bugsnag/bugsnag-cli/pkg/log"
"github.com/bugsnag/bugsnag-cli/pkg/options"
"github.com/bugsnag/bugsnag-cli/pkg/upload"
Expand Down Expand Up @@ -40,8 +41,28 @@ func main() {
logger.Info("Performing dry run - no data will be sent to BugSnag")
}

dataAccessService := data_access.NewService()

switch kongCtx.Command() {

case "create project":
err := dataAccessService.Projects.Create(commands, logger)
if err != nil {
logger.Fatal(err.Error())
}

case "get project":
err := dataAccessService.Projects.Get(commands, logger)
if err != nil {
logger.Fatal(err.Error())
}

case "update project":
err := dataAccessService.Projects.Update(commands, logger)
if err != nil {
logger.Fatal(err.Error())
}

case "upload all <path>":

if commands.ApiKey == "" {
Expand Down
40 changes: 40 additions & 0 deletions pkg/data_access/data_access_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package data_access

import (
"net/http"
"os"
"time"

"github.com/bugsnag/bugsnag-cli/pkg/log"
"github.com/bugsnag/bugsnag-cli/pkg/options"
)

// Projects defines the project-related behaviour exposed by the data access layer.
type Projects interface {
Create(globalOptions options.CLI, logger *log.LoggerWrapper) error
Get(globalOptions options.CLI, logger *log.LoggerWrapper) error
Update(globalOptions options.CLI, logger *log.LoggerWrapper) error
}

type Service struct {
httpClient *http.Client
bugsnagOrg BugsnagOrganization

// Projects provides access to project-related operations, e.g. dataAccessService.Projects.Create(...).
Projects Projects
}

func NewService() *Service {
s := &Service{
httpClient: &http.Client{Timeout: 60 * time.Second},
bugsnagOrg: BugsnagOrganization{
ID: os.Getenv("BUGSNAG_ACCOUNT_ID"),
AuthToken: os.Getenv("BUGSNAG_CLI_PERSONAL_AUTH_TOKEN"),
},
}

// Wire a concrete ProjectClient (defined in projects.go) that delegates to this Service's shared state.
s.Projects = &ProjectClient{service: s}

return s
}
174 changes: 174 additions & 0 deletions pkg/data_access/projects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package data_access

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/bugsnag/bugsnag-cli/pkg/log"
"github.com/bugsnag/bugsnag-cli/pkg/options"
)

const baseURL = "http://localhost:3000"

type BugsnagOrganization struct {
ID string
AuthToken string
}

type BugsnagProject struct {
Name string `json:"name"`
Type string `json:"type"`
GlobalGrouping []string `json:"global_grouping,omitempty"`
LocationGrouping []string `json:"location_grouping,omitempty"`
DiscardedAppVersions []string `json:"discarded_app_versions,omitempty"`
DiscardedErrors []string `json:"discarded_errors,omitempty"`
URLWhitelist []string `json:"url_whitelist,omitempty"`
IgnoreOldBrowsers bool `json:"ignore_old_browsers,omitempty"`
IgnoredBrowserVersions map[string]any `json:"ignored_browser_versions,omitempty"`
ResolveOnDeploy bool `json:"resolve_on_deploy,omitempty"`
CollaboratorIDs []string `json:"collaborator_ids,omitempty"`
}

// ProjectClient implements project-related operations using a shared Service.
type ProjectClient struct {
service *Service
}

func (p *ProjectClient) Create(globalOptions options.CLI, logger *log.LoggerWrapper) error {
if err := p.service.checkEnvironmentVariables(); err != nil {
return err
}

if globalOptions.Create.Project.Name == "" || globalOptions.Create.Project.Type == "" {
return fmt.Errorf("project name and type must be provided to create a new project")
}

project := globalOptions.Create.Project
createProjectURL := fmt.Sprintf("%s/organizations/%s/projects", baseURL, p.service.bugsnagOrg.ID)
bugsnagProject := BugsnagProject{Name: project.Name, Type: project.Type, IgnoreOldBrowsers: project.IgnoreOldBrowsers}

jsonBody, err := json.Marshal(bugsnagProject)
if err != nil {
return err
}

req, err := http.NewRequest(http.MethodPost, createProjectURL, bytes.NewBuffer(jsonBody))
if err != nil {
return err
}

responseBody, err := p.service.sendRequest(req)
if err != nil {
return err
}
logger.Info("Successfully created a new BugSnag project with the following details:\n" + responseBody)

return nil
}

func (p *ProjectClient) Get(globalOptions options.CLI, logger *log.LoggerWrapper) error {
if err := p.service.checkEnvironmentVariables(); err != nil {
return err
}

if globalOptions.Get.Project.ID == "" {
return fmt.Errorf("project ID must be provided to get project details")
}

projectID := globalOptions.Get.Project.ID
getProjectURL := fmt.Sprintf("%s/projects/%s", baseURL, projectID)

req, err := http.NewRequest(http.MethodGet, getProjectURL, nil)
if err != nil {
return err
}

responseBody, err := p.service.sendRequest(req)
if err != nil {
return err
}
logger.Info("Successfully retrieved details for a BugSnag project:\n" + responseBody)

return nil
}

func (p *ProjectClient) Update(globalOptions options.CLI, logger *log.LoggerWrapper) error {
if err := p.service.checkEnvironmentVariables(); err != nil {
return err
}

if globalOptions.Update.Project.ID == "" {
return fmt.Errorf("project ID must be provided to update a project")
}

if globalOptions.Update.Project.Name == "" || globalOptions.Update.Project.Type == "" {
return fmt.Errorf("project name and type must be provided to update a project")
}

project := globalOptions.Update.Project

bugsnagProject := BugsnagProject{
Name: project.Name,
Type: project.Type,
GlobalGrouping: project.GlobalGrouping,
LocationGrouping: project.LocationGrouping,
DiscardedAppVersions: project.DiscardedAppVersions,
DiscardedErrors: project.DiscardedErrors,
URLWhitelist: project.URLWhitelist,
IgnoreOldBrowsers: project.IgnoreOldBrowsers,
IgnoredBrowserVersions: project.IgnoredBrowserVersions,
ResolveOnDeploy: project.ResolveOnDeploy,
CollaboratorIDs: project.CollaboratorIDs,
}

jsonBody, err := json.Marshal(bugsnagProject)
if err != nil {
return err
}

req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/projects/%s", baseURL, project.ID), bytes.NewBuffer(jsonBody))
if err != nil {
return err
}

responseBody, err := p.service.sendRequest(req)
if err != nil {
return err
}
logger.Info("Successfully updated BugSnag project with the following details:\n" + responseBody)

return nil
}

func (s *Service) checkEnvironmentVariables() error {
if s.bugsnagOrg.ID == "" {
return fmt.Errorf("missing required environment variable BUGSNAG_ACCOUNT_ID")
}

if s.bugsnagOrg.AuthToken == "" {
return fmt.Errorf("missing required environment variable BUGSNAG_CLI_PERSONAL_AUTH_TOKEN")
}

return nil
}

func (s *Service) sendRequest(req *http.Request) (string, error) {
req.Header.Set("X-Bugsnag-Api", "true")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("token %s", s.bugsnagOrg.AuthToken))

resp, err := s.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
13 changes: 13 additions & 0 deletions pkg/options/data_access/data_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package data_access

type Create struct {
Project Project `cmd:"project" help:"Create a project"`
}

type Get struct {
Project Project `cmd:"project" help:"Get a project via it's ID'"`
}

type Update struct {
Project Project `cmd:"project" help:"Create a project"`
}
31 changes: 31 additions & 0 deletions pkg/options/data_access/projects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package data_access

type Project struct {
ID string `help:"The ID of the project to retrieve"`
Name string `help:"The name of the project"`
Type string `help:"The project type (e.g. 'js', 'android', 'ios')"`
GlobalGrouping []string `help:"List of error classes to group globally by class (global_grouping)"`
LocationGrouping []string `help:"List of error classes to group by context (location_grouping)"`
DiscardedAppVersions []string `help:"App versions to discard events for (discarded_app_versions). Supports regex and semver ranges"`
DiscardedErrors []string `help:"Error classes to discard events for (discarded_errors)"`
URLWhitelist []string `help:"List of whitelisted script source domains (url_whitelist)"`
IgnoreOldBrowsers bool `help:"Whether to ignore old browsers for this project"`
IgnoredBrowserVersions map[string]any `help:"Ignored browser versions for JS projects (ignored_browser_versions)"`
ResolveOnDeploy bool `help:"Whether to mark all errors as fixed on deploy (resolve_on_deploy)"`
CollaboratorIDs []string `help:"List of collaborator IDs to set on the project (collaborator_ids)"`
}

type ProjectActions struct {
Create createProject `cmd:"" help:"Create a project"`
Get getProject `cmd:"" help:"Get a project"`
}

type createProject struct {
Name string `help:"The name of the project" required:""`
Type string `help:"The project type (e.g. 'js', 'android', 'ios')" required:""`
IgnoreOldBrowsers bool `help:"Whether to ignore old browsers for this project" default:"true"`
}

type getProject struct {
ProjectID string `help:"The ID of the project to retrieve" required:""`
}
4 changes: 4 additions & 0 deletions pkg/options/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package options

import (
"github.com/bugsnag/bugsnag-cli/pkg/options/data_access"
"github.com/bugsnag/bugsnag-cli/pkg/utils"
)

Expand Down Expand Up @@ -147,6 +148,9 @@ type CLI struct {
CreateAndroidBuildId CreateAndroidBuildId `cmd:"" help:"Generate a reproducible Build ID from .dex files"`
CreateBuild CreateBuild `cmd:"" help:"Provide extra information whenever you build, release, or deploy your application"`
Upload Upload `cmd:"" help:"Upload symbol/mapping files"`
Create data_access.Create `cmd:"" help:"Create stuff"`
Get data_access.Get `cmd:"" help:"Get stuff"`
Update data_access.Update `cmd:"" help:"Update stuff"`
}

type CreateAndroidBuildId struct {
Expand Down