diff --git a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx index ee5d9bf77..c60291ae7 100644 --- a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx +++ b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessSourceDialog.tsx @@ -148,6 +148,9 @@ function mapAPIKeyExtraSettingToField( case "OKTA": if (settingKey === "domain") return "oktaDomain"; break; + case "BETTER_STACK": + if (settingKey === "teamName") return "betterStackTeamName"; + break; } return null; } diff --git a/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx b/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx new file mode 100644 index 000000000..4786b6da1 --- /dev/null +++ b/packages/ui/src/Atoms/ThirdParties/BetterStack.tsx @@ -0,0 +1,16 @@ +import type { ComponentProps } from "react"; + +export function BetterStack(props: ComponentProps<"svg">) { + return ( + + + + ); +} diff --git a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx index 333a8b8a6..a22681ac5 100644 --- a/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx +++ b/packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx @@ -16,6 +16,7 @@ import type { ComponentProps, FC } from "react"; import { Anthropic } from "./Anthropic"; import { Asana } from "./Asana"; +import { BetterStack } from "./BetterStack"; import { Bitbucket } from "./Bitbucket"; import { Brex } from "./Brex"; import { Clerk } from "./Clerk"; @@ -57,6 +58,7 @@ import { Zendesk } from "./Zendesk"; const thirdParties: Record>> = { ANTHROPIC: Anthropic, ASANA: Asana, + BETTER_STACK: BetterStack, BITBUCKET: Bitbucket, BREX: Brex, CLERK: Clerk, diff --git a/packages/ui/src/Atoms/ThirdParties/index.ts b/packages/ui/src/Atoms/ThirdParties/index.ts index 5a11c3007..ba7c824da 100644 --- a/packages/ui/src/Atoms/ThirdParties/index.ts +++ b/packages/ui/src/Atoms/ThirdParties/index.ts @@ -1,5 +1,6 @@ export { Anthropic } from "./Anthropic"; export { Asana } from "./Asana"; +export { BetterStack } from "./BetterStack"; export { Bitbucket } from "./Bitbucket"; export { Brex } from "./Brex"; export { Clerk } from "./Clerk"; diff --git a/pkg/accessreview/drivers/better_stack.go b/pkg/accessreview/drivers/better_stack.go new file mode 100644 index 000000000..30f7da891 --- /dev/null +++ b/pkg/accessreview/drivers/better_stack.go @@ -0,0 +1,225 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package drivers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "go.probo.inc/probo/pkg/coredata" +) + +// betterStackTeamMembersEndpoint is the Better Stack Uptime API team-members +// resource. The team-members reference documents this apex host and returns +// its pagination links on the same host, so the driver pins every page +// request to it (instead of following the response's `next` URL) to avoid a +// cross-host redirect that would drop the Authorization header. +const betterStackTeamMembersEndpoint = "https://betterstack.com/api/v2/team-members" + +// BetterStackDriver fetches team members and pending invitations from the +// Better Stack Uptime API via Bearer token-authenticated REST requests. The +// teamName scopes the listing; it is required when authenticating with a +// global API token and ignored for team-scoped tokens. +type BetterStackDriver struct { + httpClient *http.Client + teamName string +} + +var _ Driver = (*BetterStackDriver)(nil) + +type betterStackTeamMembersResponse struct { + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes struct { + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + CreatedAt string `json:"created_at"` + InvitedAt string `json:"invited_at"` + Role string `json:"role"` + } `json:"attributes"` + } `json:"data"` + Pagination struct { + Next *string `json:"next"` + } `json:"pagination"` +} + +func NewBetterStackDriver(httpClient *http.Client, teamName string) *BetterStackDriver { + return &BetterStackDriver{ + httpClient: &http.Client{ + Transport: &retryRoundTripper{ + next: httpClient.Transport, + maxRetries: 3, + }, + }, + teamName: teamName, + } +} + +func (d *BetterStackDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) { + var records []AccountRecord + + page := 1 + + for range maxPaginationPages { + resp, err := d.fetchTeamMembersPage(ctx, page) + if err != nil { + return nil, err + } + + for _, member := range resp.Data { + if member.Attributes.Email == "" { + continue + } + + record := AccountRecord{ + Email: member.Attributes.Email, + FullName: strings.TrimSpace(member.Attributes.FirstName + " " + member.Attributes.LastName), + Role: betterStackRole(member.Attributes.Role), + Active: betterStackActive(member.Type), + IsAdmin: betterStackIsAdmin(member.Attributes.Role), + MFAStatus: coredata.MFAStatusUnknown, + AuthMethod: coredata.AccessEntryAuthMethodUnknown, + AccountType: coredata.AccessEntryAccountTypeUser, + ExternalID: member.ID, + } + + if t, ok := parseBetterStackTimestamp(member.Attributes.CreatedAt); ok { + record.CreatedAt = &t + } else if t, ok := parseBetterStackTimestamp(member.Attributes.InvitedAt); ok { + // Invitations carry invited_at but no created_at; keep the + // first-seen timestamp in CreatedAt for review context. + record.CreatedAt = &t + } + + records = append(records, record) + } + + if resp.Pagination.Next == nil || *resp.Pagination.Next == "" { + return records, nil + } + + page++ + } + + return nil, fmt.Errorf("cannot list all better stack team members: %w", ErrPaginationLimitReached) +} + +func (d *BetterStackDriver) fetchTeamMembersPage( + ctx context.Context, + page int, +) (*betterStackTeamMembersResponse, error) { + endpoint, err := url.Parse(betterStackTeamMembersEndpoint) + if err != nil { + return nil, fmt.Errorf("cannot parse better stack team members URL: %w", err) + } + + q := endpoint.Query() + q.Set("page", strconv.Itoa(page)) + + if d.teamName != "" { + q.Set("team_name", d.teamName) + } + + endpoint.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmt.Errorf("cannot create better stack team members request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + httpResp, err := d.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot execute better stack team members request: %w", err) + } + + defer func() { _ = httpResp.Body.Close() }() + + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + return nil, fmt.Errorf("cannot fetch better stack team members: unexpected status %d", httpResp.StatusCode) + } + + var resp betterStackTeamMembersResponse + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + return nil, fmt.Errorf("cannot decode better stack team members response: %w", err) + } + + return &resp, nil +} + +// betterStackRole maps a Better Stack role token to a human-readable label. +// Unknown roles (including the Enterprise "custom" roles) are passed through +// unchanged so the reviewer still sees the source value. +func betterStackRole(role string) string { + switch role { + case "admin": + return "Admin" + case "billing_admin": + return "Billing admin" + case "team_lead": + return "Team lead" + case "responder": + return "Responder" + case "member": + return "Member" + default: + return role + } +} + +// betterStackIsAdmin flags roles with administrative control over team access: +// admin (full control) and team_lead (can manage team members and roles). +// billing_admin is billing-only, so it is not flagged. +func betterStackIsAdmin(role string) bool { + return role == "admin" || role == "team_lead" +} + +// betterStackActive maps the member record type to an explicit active signal: +// confirmed members are active, pending invitations are not, and unknown +// types leave the signal nil rather than fabricate one. +func betterStackActive(memberType string) *bool { + switch memberType { + case "team_member": + return new(true) + case "team_member_invitation": + return new(false) + default: + return nil + } +} + +func parseBetterStackTimestamp(value string) (time.Time, bool) { + if value == "" { + return time.Time{}, false + } + + // time.Parse accepts a fractional second even when the layout omits it, + // so RFC3339 covers Better Stack's ".000Z" timestamps too. + t, err := time.Parse(time.RFC3339, value) + if err != nil { + return time.Time{}, false + } + + return t, true +} diff --git a/pkg/accessreview/drivers/better_stack_test.go b/pkg/accessreview/drivers/better_stack_test.go new file mode 100644 index 000000000..482379e63 --- /dev/null +++ b/pkg/accessreview/drivers/better_stack_test.go @@ -0,0 +1,135 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package drivers + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.probo.inc/probo/pkg/coredata" +) + +func TestBetterStackDriver(t *testing.T) { + t.Parallel() + + rec := newRecorder(t, "testdata/better_stack", "BETTER_STACK_TOKEN") + + teamName := os.Getenv("BETTER_STACK_TEAM_NAME") + if teamName == "" { + teamName = "acme" + } + + client := newVCRClient(rec, bearerAuth(os.Getenv("BETTER_STACK_TOKEN"))) + + records, err := NewBetterStackDriver(client, teamName).ListAccounts(context.Background()) + require.NoError(t, err) + assert.Len(t, records, 1) + + r := records[0] + assert.Equal(t, "alice@example.com", r.Email) + assert.Equal(t, "Alice Smith", r.FullName) + assert.Equal(t, "Admin", r.Role) + assert.True(t, r.IsAdmin) + assert.Equal(t, "101", r.ExternalID) + require.NotNil(t, r.Active) + assert.True(t, *r.Active) + require.NotNil(t, r.CreatedAt) + assert.Equal(t, coredata.MFAStatusUnknown, r.MFAStatus) +} + +func TestBetterStackDriverPagination(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/api/v2/team-members", r.URL.Path) + assert.Equal(t, "acme", r.URL.Query().Get("team_name")) + + w.Header().Set("Content-Type", "application/json") + + if r.URL.Query().Get("page") == "2" { + _, _ = w.Write([]byte(`{"data":[{"id":"201","type":"team_member_invitation","attributes":{"email":"charlie@example.com","invited_at":"2023-10-28T12:00:00.000Z","role":"member"}}],"pagination":{"next":null}}`)) + + return + } + + _, _ = w.Write([]byte(`{"data":[{"id":"101","type":"team_member","attributes":{"email":"alice@example.com","first_name":"Alice","last_name":"Smith","created_at":"2023-10-26T10:00:00.000Z","role":"admin"}},{"id":"102","type":"team_member","attributes":{"email":"bob@example.com","first_name":"Bob","last_name":"","created_at":"2023-10-27T11:00:00.000Z","role":"responder"}}],"pagination":{"next":"https://betterstack.com/api/v2/team-members?page=2&team_name=acme"}}`)) + })) + defer srv.Close() + + client := &http.Client{Transport: &hostRewriter{target: srv.URL}} + + records, err := NewBetterStackDriver(client, "acme").ListAccounts(context.Background()) + require.NoError(t, err) + require.Len(t, records, 3) + + assert.Equal(t, "alice@example.com", records[0].Email) + assert.Equal(t, "Alice Smith", records[0].FullName) + assert.True(t, *records[0].Active) + assert.Equal(t, "bob@example.com", records[1].Email) + assert.Equal(t, "Bob", records[1].FullName) + assert.Equal(t, "charlie@example.com", records[2].Email) + require.NotNil(t, records[2].Active) + assert.False(t, *records[2].Active) +} + +func TestBetterStackDriverListAccountsError(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"errors":"invalid token"}`)) + })) + defer srv.Close() + + client := &http.Client{Transport: &hostRewriter{target: srv.URL}} + + _, err := NewBetterStackDriver(client, "acme").ListAccounts(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "unexpected status 401") +} + +func TestBetterStackRole(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + want string + isAdmin bool + }{ + {"admin", "Admin", true}, + {"billing_admin", "Billing admin", false}, + {"team_lead", "Team lead", true}, + {"responder", "Responder", false}, + {"member", "Member", false}, + {"custom", "custom", false}, + {"future_role", "future_role", false}, + } + + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, c.want, betterStackRole(c.in)) + assert.Equal(t, c.isAdmin, betterStackIsAdmin(c.in)) + }) + } +} diff --git a/pkg/accessreview/drivers/name_resolver.go b/pkg/accessreview/drivers/name_resolver.go index 182f04c2d..df34050c4 100644 --- a/pkg/accessreview/drivers/name_resolver.go +++ b/pkg/accessreview/drivers/name_resolver.go @@ -706,6 +706,21 @@ func (r *resendNameResolver) ResolveInstanceName(_ context.Context) (string, err return "Resend", nil } +// betterStackNameResolver returns the Better Stack team name captured when +// the API-key connector was created. The team name is the human-readable +// instance identifier, so no HTTP call is required. +type betterStackNameResolver struct { + teamName string +} + +func NewBetterStackNameResolver(teamName string) NameResolver { + return &betterStackNameResolver{teamName: teamName} +} + +func (r *betterStackNameResolver) ResolveInstanceName(_ context.Context) (string, error) { + return r.teamName, nil +} + // gitlabNameResolver resolves the GitLab group name. type gitlabNameResolver struct { httpClient *http.Client diff --git a/pkg/accessreview/drivers/testdata/better_stack.yaml b/pkg/accessreview/drivers/testdata/better_stack.yaml new file mode 100644 index 000000000..d4098d8d4 --- /dev/null +++ b/pkg/accessreview/drivers/testdata/better_stack.yaml @@ -0,0 +1,33 @@ +--- +version: 2 +interactions: + - id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 0 + host: betterstack.com + form: + page: + - "1" + team_name: + - acme + headers: + Accept: + - application/json + url: https://betterstack.com/api/v2/team-members?page=1&team_name=acme + method: GET + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: true + body: '{"data":[{"id":"101","type":"team_member","attributes":{"email":"alice@example.com","first_name":"Alice","last_name":"Smith","created_at":"2026-06-09T13:24:53.712Z","role":"admin","mobile_app_platforms":[]}}],"pagination":{"first":"https://betterstack.com/api/v2/team-members?page=1&team_name=acme","last":"https://betterstack.com/api/v2/team-members?page=1&team_name=acme","prev":null,"next":null}}' + headers: + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 100ms diff --git a/pkg/connector/provider/better_stack.go b/pkg/connector/provider/better_stack.go new file mode 100644 index 000000000..c246cd79e --- /dev/null +++ b/pkg/connector/provider/better_stack.go @@ -0,0 +1,65 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package provider + +import ( + "context" + "fmt" + "net/http" + "strings" + + "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/accessreview/drivers" + "go.probo.inc/probo/pkg/coredata" +) + +// betterStackRegistration wires the Better Stack Uptime access-review +// connector. Better Stack has no third-party OAuth app for listing team +// members (its OAuth is an end-user MCP sign-in), so the connector is +// API-key only: the operator supplies a Bearer API token plus the team +// name that scopes the team-members listing. +func betterStackRegistration() *Registration { + return &Registration{ + Provider: coredata.ConnectorProviderBetterStack, + DisplayName: "Better Stack", + SupportsAPIKey: true, + ProbeURL: "https://betterstack.com/api/v2/team-members", + ExtraSettings: []ExtraSetting{ + {Key: "teamName", Label: "Team Name", Required: true}, + }, + NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) { + s, err := coredata.ConnectorSettings[coredata.BetterStackConnectorSettings](conn) + if err != nil { + return nil, fmt.Errorf("cannot read better stack connector settings: %w", err) + } + + teamName := strings.TrimSpace(s.TeamName) + if teamName == "" { + return nil, fmt.Errorf("cannot create better stack driver: team_name is required") + } + + return drivers.NewBetterStackDriver(c, teamName), nil + }, + NewNameResolver: func(ctx context.Context, _ *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver { + s, err := coredata.ConnectorSettings[coredata.BetterStackConnectorSettings](conn) + if err != nil { + logger.ErrorCtx(ctx, "cannot read better stack connector settings", log.Error(err)) + return nil + } + + return drivers.NewBetterStackNameResolver(strings.TrimSpace(s.TeamName)) + }, + } +} diff --git a/pkg/connector/provider/builtin.go b/pkg/connector/provider/builtin.go index 8a15c6856..7092c7919 100644 --- a/pkg/connector/provider/builtin.go +++ b/pkg/connector/provider/builtin.go @@ -24,6 +24,7 @@ func NewBuiltinRegistry() *Registry { for _, reg := range []*Registration{ anthropicRegistration(), asanaRegistration(), + betterStackRegistration(), bitbucketRegistration(), brexRegistration(), clerkRegistration(), diff --git a/pkg/coredata/connector_provider.go b/pkg/coredata/connector_provider.go index 233108445..354e4c183 100644 --- a/pkg/coredata/connector_provider.go +++ b/pkg/coredata/connector_provider.go @@ -39,6 +39,7 @@ const ( ConnectorProviderSentry ConnectorProvider = "SENTRY" ConnectorProviderSigNoz ConnectorProvider = "SIGNOZ" ConnectorProviderSupabase ConnectorProvider = "SUPABASE" + ConnectorProviderBetterStack ConnectorProvider = "BETTER_STACK" ConnectorProviderGitHub ConnectorProvider = "GITHUB" ConnectorProviderIntercom ConnectorProvider = "INTERCOM" ConnectorProviderResend ConnectorProvider = "RESEND" @@ -87,6 +88,7 @@ func ConnectorProviders() []ConnectorProvider { ConnectorProviderSentry, ConnectorProviderSigNoz, ConnectorProviderSupabase, + ConnectorProviderBetterStack, ConnectorProviderGitHub, ConnectorProviderIntercom, ConnectorProviderResend, @@ -131,6 +133,7 @@ func (v ConnectorProvider) IsValid() bool { ConnectorProviderSentry, ConnectorProviderSigNoz, ConnectorProviderSupabase, + ConnectorProviderBetterStack, ConnectorProviderGitHub, ConnectorProviderIntercom, ConnectorProviderResend, diff --git a/pkg/coredata/connector_settings.go b/pkg/coredata/connector_settings.go index dc2298840..3c2d34ab7 100644 --- a/pkg/coredata/connector_settings.go +++ b/pkg/coredata/connector_settings.go @@ -136,6 +136,10 @@ type ( ZendeskConnectorSettings struct { Subdomain string `json:"subdomain"` } + + BetterStackConnectorSettings struct { + TeamName string `json:"team_name"` + } ) // GrantType returns the OAuth2 grant type recorded on the connector's diff --git a/pkg/coredata/migrations/20260608T417263Z.sql b/pkg/coredata/migrations/20260608T417263Z.sql new file mode 100644 index 000000000..1bf9dd5b4 --- /dev/null +++ b/pkg/coredata/migrations/20260608T417263Z.sql @@ -0,0 +1,15 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'BETTER_STACK'; diff --git a/pkg/server/api/console/v1/connector_settings.go b/pkg/server/api/console/v1/connector_settings.go index 686ec61f5..1da375490 100644 --- a/pkg/server/api/console/v1/connector_settings.go +++ b/pkg/server/api/console/v1/connector_settings.go @@ -154,6 +154,12 @@ func apiKeyConnectorSettings(input types.CreateAPIKeyConnectorInput) (json.RawMe } return json.Marshal(&coredata.OktaConnectorSettings{Domain: domain}) + case coredata.ConnectorProviderBetterStack: + if input.BetterStackTeamName == nil || *input.BetterStackTeamName == "" { + return nil, fmt.Errorf("cannot create better stack connector: betterStackTeamName is required") + } + + return json.Marshal(&coredata.BetterStackConnectorSettings{TeamName: *input.BetterStackTeamName}) } return nil, nil diff --git a/pkg/server/api/console/v1/graphql/connector.graphql b/pkg/server/api/console/v1/graphql/connector.graphql index 4b574a57b..d255299db 100644 --- a/pkg/server/api/console/v1/graphql/connector.graphql +++ b/pkg/server/api/console/v1/graphql/connector.graphql @@ -28,6 +28,10 @@ enum ConnectorProvider @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSigNoz") SUPABASE @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSupabase") + BETTER_STACK + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderBetterStack" + ) GITHUB @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGitHub") INTERCOM @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderIntercom") @@ -141,6 +145,7 @@ input CreateAPIKeyConnectorInput { posthogRegion: String posthogInstanceUrl: String oktaDomain: String + betterStackTeamName: String } type CreateAPIKeyConnectorPayload {