Fix PostHog source disconnected on EU cloud OAuth

The connection-status probe and the access-review driver each had
their own copy of the "try us.posthog.com, then eu.posthog.com"
region-discovery loop, and they drifted. The driver skips a region
that rejects the token (wrong region) and uses the one that answers
2xx; the probe instead returned "credential rejected" on the first
region's 401/403, before ever trying the second.

PostHog Cloud US and EU are separate deployments, so an EU OAuth
token is a 401 on us.posthog.com, which is probed first. The probe
bailed there and marked the source disconnected, while access-review
campaigns -- which use the driver -- kept working.

Delete the probe's copy and delegate to the driver's now-exported
ResolvePostHogRegion, the single resolver the campaign also uses. It
flags a credential every region rejected (ErrPostHogCredentialRejected)
apart from a transient failure on the token's own region, so the probe
marks a source disconnected only for a genuinely dead token and does
not flap on a passing 5xx.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-07-22 22:32:51 +02:00
parent 386962dff6
commit 3805afc806
3 changed files with 146 additions and 45 deletions

View File

@@ -24,6 +24,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -31,6 +32,7 @@ import (
"slices"
"strings"
"go.probo.inc/probo/pkg/accessreview/drivers"
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata"
)
@@ -43,9 +45,6 @@ const (
linearGraphQLEndpoint = "https://api.linear.app/graphql"
mondayGraphQLEndpoint = "https://api.monday.com/v2"
railwayGraphQLEndpoint = "https://backboard.railway.com/graphql/v2"
posthogOrganizationPath = "/api/organizations/@current/"
posthogUSBaseURL = "https://us.posthog.com"
posthogEUBaseURL = "https://eu.posthog.com"
crispAPIBaseURL = "https://api.crisp.chat/v1"
crispTierHeader = "X-Crisp-Tier"
crispTierValue = "plugin"
@@ -406,7 +405,7 @@ func buildPostHogProbeURL(conn *coredata.Connector) (string, error) {
return "", nil
}
return url.JoinPath(baseURL, posthogOrganizationPath)
return url.JoinPath(baseURL, drivers.PostHogOrganizationPath)
}
func probeLinear(
@@ -597,44 +596,22 @@ func probePostHog(
return err
}
// Explicit host (API-key region or self-hosted): probe it directly.
if probeURL != "" {
return probeGET(ctx, httpClient, probeURL)
}
for _, host := range []string{posthogUSBaseURL, posthogEUBaseURL} {
endpoint, err := url.JoinPath(host, posthogOrganizationPath)
if err != nil {
continue
// Cloud OAuth (empty BaseURL): reuse the driver's region resolver so the
// probe and the campaign never drift. Only a credential every region
// rejected is disconnected; a transient failure on the token's own region
// stays connected rather than flapping the badge.
if _, err := drivers.ResolvePostHogRegion(ctx, httpClient); err != nil {
if errors.Is(err, drivers.ErrPostHogCredentialRejected) {
return fmt.Errorf("cannot probe posthog: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
continue
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
if ctx.Err() != nil {
return fmt.Errorf("cannot probe posthog region: %w", ctx.Err())
}
continue
}
status := resp.StatusCode
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if status == http.StatusUnauthorized || status == http.StatusForbidden {
return fmt.Errorf("credential rejected: status %d", status)
}
if status >= http.StatusOK && status < http.StatusMultipleChoices {
return nil
}
return nil
}
return fmt.Errorf("credential rejected: no posthog region accepted the connection")
return nil
}

View File

@@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/accessreview/drivers"
"go.probo.inc/probo/pkg/coredata"
)
@@ -120,6 +121,91 @@ func TestBuildPostHogProbeURL(t *testing.T) {
assert.Equal(t, "https://us.posthog.com/api/organizations/@current/", probeURL)
}
func TestProbePostHog(t *testing.T) {
t.Parallel()
// A cloud OAuth connection carries no region (empty BaseURL): the token is
// valid on exactly one PostHog region and the other rejects it with
// 401/403. The probe must try every region and only report the credential
// rejected when none accept it — mirroring the access-review driver — so an
// EU token hitting us.posthog.com (probed first) does not falsely mark the
// source disconnected while its access reviews keep working. A transient
// 5xx on the token's own region is inconclusive, not a rejection.
cases := []struct {
name string
baseURL string
hostStatus map[string]int
wantErr bool
wantRejected bool
}{
{
name: "explicit region accepts",
baseURL: "https://us.posthog.com",
hostStatus: map[string]int{"us.posthog.com": http.StatusOK},
wantErr: false,
},
{
name: "explicit region rejects",
baseURL: "https://us.posthog.com",
hostStatus: map[string]int{"us.posthog.com": http.StatusUnauthorized},
wantErr: true,
},
{
name: "oauth EU token: US refuses, EU accepts",
baseURL: "",
hostStatus: map[string]int{"us.posthog.com": http.StatusUnauthorized, "eu.posthog.com": http.StatusOK},
wantErr: false,
},
{
name: "oauth transient: US refuses, EU errors",
baseURL: "",
hostStatus: map[string]int{"us.posthog.com": http.StatusUnauthorized, "eu.posthog.com": http.StatusInternalServerError},
wantErr: false,
},
{
name: "oauth dead token: every region refuses",
baseURL: "",
hostStatus: map[string]int{"us.posthog.com": http.StatusForbidden, "eu.posthog.com": http.StatusForbidden},
wantErr: true,
wantRejected: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
status, ok := tc.hostStatus[r.URL.Host]
if !ok {
status = http.StatusNotFound
}
return &http.Response{StatusCode: status, Body: http.NoBody, Header: make(http.Header)}, nil
})}
conn := &coredata.Connector{Provider: coredata.ConnectorProviderPostHog}
require.NoError(t, conn.SetSettings(&coredata.PostHogConnectorSettings{BaseURL: tc.baseURL}))
err := probePostHog(context.Background(), client, conn)
if !tc.wantErr {
require.NoError(t, err)
return
}
require.Error(t, err)
// A credential every region refused must surface the sentinel so the
// probe distinguishes it from an inconclusive/transient failure.
if tc.wantRejected {
require.ErrorIs(t, err, drivers.ErrPostHogCredentialRejected)
}
})
}
}
func TestBuildScalewayProbeURL(t *testing.T) {
t.Parallel()