upgrade deps; rewrite smtp session
This commit is contained in:
291
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
291
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
@@ -3,15 +3,12 @@ package sentry
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -20,6 +17,9 @@ import (
|
||||
"github.com/getsentry/sentry-go/internal/debug"
|
||||
)
|
||||
|
||||
// The identifier of the SDK.
|
||||
const sdkIdentifier = "sentry.go"
|
||||
|
||||
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||
//
|
||||
@@ -29,6 +29,11 @@ import (
|
||||
// stack trace is often the most useful information.
|
||||
const maxErrorDepth = 10
|
||||
|
||||
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
|
||||
// meant to bound memory usage and prevent too large transaction events that
|
||||
// would be rejected by Sentry.
|
||||
const defaultMaxSpans = 1000
|
||||
|
||||
// hostname is the host name reported by the kernel. It is precomputed once to
|
||||
// avoid syscalls when capturing events.
|
||||
//
|
||||
@@ -60,7 +65,7 @@ func (r *lockedRand) Float64() float64 {
|
||||
// other hand, the source returned from rand.NewSource is not safe for
|
||||
// concurrent use, so we need to couple its use with a sync.Mutex.
|
||||
var rng = &lockedRand{
|
||||
//#nosec G404 -- We are fine using transparent, non-secure value here.
|
||||
// #nosec G404 -- We are fine using transparent, non-secure value here.
|
||||
r: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||
}
|
||||
|
||||
@@ -74,7 +79,7 @@ type usageError struct {
|
||||
|
||||
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
|
||||
// can be enabled by either using Logger.SetOutput directly or with Debug client option.
|
||||
var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags)
|
||||
var Logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
|
||||
|
||||
// EventProcessor is a function that processes an event.
|
||||
// Event processors are used to change an event before it is sent to Sentry.
|
||||
@@ -122,18 +127,31 @@ type ClientOptions struct {
|
||||
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
|
||||
// empty string.
|
||||
SampleRate float64
|
||||
// Enable performance tracing.
|
||||
EnableTracing bool
|
||||
// The sample rate for sampling traces in the range [0.0, 1.0].
|
||||
TracesSampleRate float64
|
||||
// Used to customize the sampling of traces, overrides TracesSampleRate.
|
||||
TracesSampler TracesSampler
|
||||
// The sample rate for profiling traces in the range [0.0, 1.0].
|
||||
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
|
||||
ProfilesSampleRate float64
|
||||
// List of regexp strings that will be used to match against event's message
|
||||
// and if applicable, caught errors type and value.
|
||||
// If the match is found, then a whole event will be dropped.
|
||||
IgnoreErrors []string
|
||||
// List of regexp strings that will be used to match against a transaction's
|
||||
// name. If a match is found, then the transaction will be dropped.
|
||||
IgnoreTransactions []string
|
||||
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
|
||||
// By default, no such data is sent.
|
||||
SendDefaultPII bool
|
||||
// BeforeSend is called before error events are sent to Sentry.
|
||||
// Use it to mutate the event or return nil to discard the event.
|
||||
// See EventProcessor if you need to mutate transactions.
|
||||
BeforeSend func(event *Event, hint *EventHint) *Event
|
||||
// BeforeSendTransaction is called before transaction events are sent to Sentry.
|
||||
// Use it to mutate the transaction or return nil to discard the transaction.
|
||||
BeforeSendTransaction func(event *Event, hint *EventHint) *Event
|
||||
// Before breadcrumb add callback.
|
||||
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
|
||||
// Integrations to be installed on the current Client, receives default
|
||||
@@ -173,8 +191,14 @@ type ClientOptions struct {
|
||||
Dist string
|
||||
// The environment to be sent with events.
|
||||
Environment string
|
||||
// Maximum number of breadcrumbs.
|
||||
// Maximum number of breadcrumbs
|
||||
// when MaxBreadcrumbs is negative then ignore breadcrumbs.
|
||||
MaxBreadcrumbs int
|
||||
// Maximum number of spans.
|
||||
//
|
||||
// See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits
|
||||
// applied during event ingestion. Events that exceed these limits might get dropped.
|
||||
MaxSpans int
|
||||
// An optional pointer to http.Client that will be used with a default
|
||||
// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
|
||||
// HTTPSProxy and CaCerts options ignored.
|
||||
@@ -192,15 +216,28 @@ type ClientOptions struct {
|
||||
HTTPSProxy string
|
||||
// An optional set of SSL certificates to use.
|
||||
CaCerts *x509.CertPool
|
||||
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
|
||||
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
||||
//
|
||||
// An additional consideration is that arguably reporting a long chain of errors
|
||||
// is of little use when debugging production errors with Sentry. The Sentry UI
|
||||
// is not optimized for long chains either. The top-level error together with a
|
||||
// stack trace is often the most useful information.
|
||||
MaxErrorDepth int
|
||||
// Default event tags. These are overridden by tags set on a scope.
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
// Client is the underlying processor that is used by the main API and Hub
|
||||
// instances. It must be created with NewClient.
|
||||
type Client struct {
|
||||
mu sync.RWMutex
|
||||
options ClientOptions
|
||||
dsn *Dsn
|
||||
eventProcessors []EventProcessor
|
||||
integrations []Integration
|
||||
sdkIdentifier string
|
||||
sdkVersion string
|
||||
// Transport is read-only. Replacing the transport of an existing client is
|
||||
// not supported, create a new client instead.
|
||||
Transport Transport
|
||||
@@ -214,8 +251,26 @@ type Client struct {
|
||||
// single goroutine) or hub methods (for concurrent programs, for example web
|
||||
// servers).
|
||||
func NewClient(options ClientOptions) (*Client, error) {
|
||||
if options.TracesSampleRate != 0.0 && options.TracesSampler != nil {
|
||||
return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive")
|
||||
// The default error event sample rate for all SDKs is 1.0 (send all).
|
||||
//
|
||||
// In Go, the zero value (default) for float64 is 0.0, which means that
|
||||
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
|
||||
// initializing the SDK with Init(ClientOptions{}) without an explicit
|
||||
// SampleRate would drop all events.
|
||||
//
|
||||
// To retain the desired default behavior, we exceptionally flip SampleRate
|
||||
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
|
||||
// anyway, and the same end result can be achieved in many other ways like
|
||||
// not initializing the SDK, setting the DSN to the empty string or using an
|
||||
// event processor that always returns nil.
|
||||
//
|
||||
// An alternative API could be such that default options don't need to be
|
||||
// the same as Go's zero values, for example using the Functional Options
|
||||
// pattern. That would either require a breaking change if we want to reuse
|
||||
// the obvious NewClient name, or a new function as an alternative
|
||||
// constructor.
|
||||
if options.SampleRate == 0.0 {
|
||||
options.SampleRate = 1.0
|
||||
}
|
||||
|
||||
if options.Debug {
|
||||
@@ -238,6 +293,14 @@ func NewClient(options ClientOptions) (*Client, error) {
|
||||
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
|
||||
}
|
||||
|
||||
if options.MaxErrorDepth == 0 {
|
||||
options.MaxErrorDepth = maxErrorDepth
|
||||
}
|
||||
|
||||
if options.MaxSpans == 0 {
|
||||
options.MaxSpans = defaultMaxSpans
|
||||
}
|
||||
|
||||
// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
|
||||
// to GODEBUG). It is not a supported feature: recognized debug options
|
||||
// may change any time.
|
||||
@@ -271,8 +334,10 @@ func NewClient(options ClientOptions) (*Client, error) {
|
||||
}
|
||||
|
||||
client := Client{
|
||||
options: options,
|
||||
dsn: dsn,
|
||||
options: options,
|
||||
dsn: dsn,
|
||||
sdkIdentifier: sdkIdentifier,
|
||||
sdkVersion: SDKVersion,
|
||||
}
|
||||
|
||||
client.setupTransport()
|
||||
@@ -294,7 +359,7 @@ func (client *Client) setupTransport() {
|
||||
// accommodate more concurrent events.
|
||||
// TODO(tracing): consider using separate buffers per
|
||||
// event type.
|
||||
if opts.TracesSampleRate != 0 || opts.TracesSampler != nil {
|
||||
if opts.EnableTracing {
|
||||
httpTransport.BufferSize = 1000
|
||||
}
|
||||
transport = httpTransport
|
||||
@@ -311,6 +376,8 @@ func (client *Client) setupIntegrations() {
|
||||
new(environmentIntegration),
|
||||
new(modulesIntegration),
|
||||
new(ignoreErrorsIntegration),
|
||||
new(ignoreTransactionsIntegration),
|
||||
new(globalTagsIntegration),
|
||||
}
|
||||
|
||||
if client.options.Integrations != nil {
|
||||
@@ -326,6 +393,10 @@ func (client *Client) setupIntegrations() {
|
||||
integration.SetupOnce(client)
|
||||
Logger.Printf("Integration installed: %s\n", integration.Name())
|
||||
}
|
||||
|
||||
sort.Slice(client.integrations, func(i, j int) bool {
|
||||
return client.integrations[i].Name() < client.integrations[j].Name()
|
||||
})
|
||||
}
|
||||
|
||||
// AddEventProcessor adds an event processor to the client. It must not be
|
||||
@@ -340,22 +411,33 @@ func (client *Client) AddEventProcessor(processor EventProcessor) {
|
||||
}
|
||||
|
||||
// Options return ClientOptions for the current Client.
|
||||
func (client Client) Options() ClientOptions {
|
||||
func (client *Client) Options() ClientOptions {
|
||||
// Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time.
|
||||
return client.options
|
||||
}
|
||||
|
||||
// CaptureMessage captures an arbitrary message.
|
||||
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.eventFromMessage(message, LevelInfo)
|
||||
event := client.EventFromMessage(message, LevelInfo)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureException captures an error.
|
||||
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.eventFromException(exception, LevelError)
|
||||
event := client.EventFromException(exception, LevelError)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureCheckIn captures a check in.
|
||||
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
|
||||
event := client.EventFromCheckIn(checkIn, monitorConfig)
|
||||
if event != nil && event.CheckIn != nil {
|
||||
client.CaptureEvent(event, nil, scope)
|
||||
return &event.CheckIn.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaptureEvent captures an event on the currently active client if any.
|
||||
//
|
||||
// The event must already be assembled. Typically code would instead use
|
||||
@@ -376,7 +458,7 @@ func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModif
|
||||
// use the Context for communicating deadline nor cancelation. All it does
|
||||
// is store the Context in the EventHint and there nil means the Context is
|
||||
// not available.
|
||||
//nolint: staticcheck
|
||||
// nolint: staticcheck
|
||||
return client.RecoverWithContext(nil, err, hint, scope)
|
||||
}
|
||||
|
||||
@@ -407,11 +489,11 @@ func (client *Client) RecoverWithContext(
|
||||
var event *Event
|
||||
switch err := err.(type) {
|
||||
case error:
|
||||
event = client.eventFromException(err, LevelFatal)
|
||||
event = client.EventFromException(err, LevelFatal)
|
||||
case string:
|
||||
event = client.eventFromMessage(err, LevelFatal)
|
||||
event = client.EventFromMessage(err, LevelFatal)
|
||||
default:
|
||||
event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
|
||||
event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
|
||||
}
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
@@ -431,16 +513,17 @@ func (client *Client) Flush(timeout time.Duration) bool {
|
||||
return client.Transport.Flush(timeout)
|
||||
}
|
||||
|
||||
func (client *Client) eventFromMessage(message string, level Level) *Event {
|
||||
// EventFromMessage creates an event from the given message string.
|
||||
func (client *Client) EventFromMessage(message string, level Level) *Event {
|
||||
if message == "" {
|
||||
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
|
||||
return client.eventFromException(err, level)
|
||||
return client.EventFromException(err, level)
|
||||
}
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
event.Message = message
|
||||
|
||||
if client.Options().AttachStacktrace {
|
||||
if client.options.AttachStacktrace {
|
||||
event.Threads = []Thread{{
|
||||
Stacktrace: NewStacktrace(),
|
||||
Crashed: false,
|
||||
@@ -451,45 +534,62 @@ func (client *Client) eventFromMessage(message string, level Level) *Event {
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) eventFromException(exception error, level Level) *Event {
|
||||
// EventFromException creates a new Sentry event from the given `error` instance.
|
||||
func (client *Client) EventFromException(exception error, level Level) *Event {
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
|
||||
err := exception
|
||||
if err == nil {
|
||||
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
|
||||
}
|
||||
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
|
||||
for i := 0; i < maxErrorDepth && err != nil; i++ {
|
||||
event.Exception = append(event.Exception, Exception{
|
||||
Value: err.Error(),
|
||||
Type: reflect.TypeOf(err).String(),
|
||||
Stacktrace: ExtractStacktrace(err),
|
||||
})
|
||||
switch previous := err.(type) {
|
||||
case interface{ Unwrap() error }:
|
||||
err = previous.Unwrap()
|
||||
case interface{ Cause() error }:
|
||||
err = previous.Cause()
|
||||
default:
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add a trace of the current stack to the most recent error in a chain if
|
||||
// it doesn't have a stack trace yet.
|
||||
// We only add to the most recent error to avoid duplication and because the
|
||||
// current stack is most likely unrelated to errors deeper in the chain.
|
||||
if event.Exception[0].Stacktrace == nil {
|
||||
event.Exception[0].Stacktrace = NewStacktrace()
|
||||
}
|
||||
|
||||
// event.Exception should be sorted such that the most recent error is last.
|
||||
reverse(event.Exception)
|
||||
event.SetException(err, client.options.MaxErrorDepth)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
|
||||
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
|
||||
if checkIn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
event := NewEvent()
|
||||
event.Type = checkInType
|
||||
|
||||
var checkInID EventID
|
||||
if checkIn.ID == "" {
|
||||
checkInID = EventID(uuid())
|
||||
} else {
|
||||
checkInID = checkIn.ID
|
||||
}
|
||||
|
||||
event.CheckIn = &CheckIn{
|
||||
ID: checkInID,
|
||||
MonitorSlug: checkIn.MonitorSlug,
|
||||
Status: checkIn.Status,
|
||||
Duration: checkIn.Duration,
|
||||
}
|
||||
event.MonitorConfig = monitorConfig
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) SetSDKIdentifier(identifier string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
client.sdkIdentifier = identifier
|
||||
}
|
||||
|
||||
func (client *Client) GetSDKIdentifier() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.sdkIdentifier
|
||||
}
|
||||
|
||||
// reverse reverses the slice a in place.
|
||||
func reverse(a []Exception) {
|
||||
for i := len(a)/2 - 1; i >= 0; i-- {
|
||||
@@ -504,34 +604,10 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
|
||||
return client.CaptureException(err, hint, scope)
|
||||
}
|
||||
|
||||
options := client.Options()
|
||||
|
||||
// The default error event sample rate for all SDKs is 1.0 (send all).
|
||||
//
|
||||
// In Go, the zero value (default) for float64 is 0.0, which means that
|
||||
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
|
||||
// initializing the SDK with Init(ClientOptions{}) without an explicit
|
||||
// SampleRate would drop all events.
|
||||
//
|
||||
// To retain the desired default behavior, we exceptionally flip SampleRate
|
||||
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
|
||||
// anyway, and the same end result can be achieved in many other ways like
|
||||
// not initializing the SDK, setting the DSN to the empty string or using an
|
||||
// event processor that always returns nil.
|
||||
//
|
||||
// An alternative API could be such that default options don't need to be
|
||||
// the same as Go's zero values, for example using the Functional Options
|
||||
// pattern. That would either require a breaking change if we want to reuse
|
||||
// the obvious NewClient name, or a new function as an alternative
|
||||
// constructor.
|
||||
if options.SampleRate == 0.0 {
|
||||
options.SampleRate = 1.0
|
||||
}
|
||||
|
||||
// Transactions are sampled by options.TracesSampleRate or
|
||||
// options.TracesSampler when they are started. All other events
|
||||
// (errors, messages) are sampled here.
|
||||
if event.Type != transactionType && !sample(options.SampleRate) {
|
||||
// options.TracesSampler when they are started. Other events
|
||||
// (errors, messages) are sampled here. Does not apply to check-ins.
|
||||
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
|
||||
Logger.Println("Event dropped due to SampleRate hit.")
|
||||
return nil
|
||||
}
|
||||
@@ -540,12 +616,19 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
|
||||
return nil
|
||||
}
|
||||
|
||||
// As per spec, transactions do not go through BeforeSend.
|
||||
if event.Type != transactionType && options.BeforeSend != nil {
|
||||
if hint == nil {
|
||||
hint = &EventHint{}
|
||||
// Apply beforeSend* processors
|
||||
if hint == nil {
|
||||
hint = &EventHint{}
|
||||
}
|
||||
if event.Type == transactionType && client.options.BeforeSendTransaction != nil {
|
||||
// Transaction events
|
||||
if event = client.options.BeforeSendTransaction(event, hint); event == nil {
|
||||
Logger.Println("Transaction dropped due to BeforeSendTransaction callback.")
|
||||
return nil
|
||||
}
|
||||
if event = options.BeforeSend(event, hint); event == nil {
|
||||
} else if event.Type != transactionType && event.Type != checkInType && client.options.BeforeSend != nil {
|
||||
// All other events
|
||||
if event = client.options.BeforeSend(event, hint); event == nil {
|
||||
Logger.Println("Event dropped due to BeforeSend callback.")
|
||||
return nil
|
||||
}
|
||||
@@ -558,6 +641,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
|
||||
|
||||
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
|
||||
if event.EventID == "" {
|
||||
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
|
||||
event.EventID = EventID(uuid())
|
||||
}
|
||||
|
||||
@@ -570,33 +654,33 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
|
||||
}
|
||||
|
||||
if event.ServerName == "" {
|
||||
if client.Options().ServerName != "" {
|
||||
event.ServerName = client.Options().ServerName
|
||||
} else {
|
||||
event.ServerName = client.options.ServerName
|
||||
|
||||
if event.ServerName == "" {
|
||||
event.ServerName = hostname
|
||||
}
|
||||
}
|
||||
|
||||
if event.Release == "" && client.Options().Release != "" {
|
||||
event.Release = client.Options().Release
|
||||
if event.Release == "" {
|
||||
event.Release = client.options.Release
|
||||
}
|
||||
|
||||
if event.Dist == "" && client.Options().Dist != "" {
|
||||
event.Dist = client.Options().Dist
|
||||
if event.Dist == "" {
|
||||
event.Dist = client.options.Dist
|
||||
}
|
||||
|
||||
if event.Environment == "" && client.Options().Environment != "" {
|
||||
event.Environment = client.Options().Environment
|
||||
if event.Environment == "" {
|
||||
event.Environment = client.options.Environment
|
||||
}
|
||||
|
||||
event.Platform = "go"
|
||||
event.Sdk = SdkInfo{
|
||||
Name: "sentry.go",
|
||||
Version: Version,
|
||||
Name: client.GetSDKIdentifier(),
|
||||
Version: SDKVersion,
|
||||
Integrations: client.listIntegrations(),
|
||||
Packages: []SdkPackage{{
|
||||
Name: "sentry-go",
|
||||
Version: Version,
|
||||
Version: SDKVersion,
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -625,19 +709,22 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
|
||||
}
|
||||
}
|
||||
|
||||
if event.sdkMetaData.transactionProfile != nil {
|
||||
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client Client) listIntegrations() []string {
|
||||
integrations := make([]string, 0, len(client.integrations))
|
||||
for _, integration := range client.integrations {
|
||||
integrations = append(integrations, integration.Name())
|
||||
func (client *Client) listIntegrations() []string {
|
||||
integrations := make([]string, len(client.integrations))
|
||||
for i, integration := range client.integrations {
|
||||
integrations[i] = integration.Name()
|
||||
}
|
||||
sort.Strings(integrations)
|
||||
return integrations
|
||||
}
|
||||
|
||||
func (client Client) integrationAlreadyInstalled(name string) bool {
|
||||
func (client *Client) integrationAlreadyInstalled(name string) bool {
|
||||
for _, integration := range client.integrations {
|
||||
if integration.Name() == name {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user