diff --git a/pkg/connector/event_log.go b/pkg/connector/event_log.go index 186e30322..94a75eb4d 100644 --- a/pkg/connector/event_log.go +++ b/pkg/connector/event_log.go @@ -10,6 +10,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/okta/okta-sdk-golang/v2/okta" "github.com/okta/okta-sdk-golang/v2/okta/query" "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" @@ -68,7 +69,7 @@ func (connector *Okta) ListEvents( logs, resp, err := connector.client.LogEvent.GetLogs(ctx, qp) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, wrapListEventsError(qp, resp, err) } // MJP each log is not guaranteed to result in a v2.Event anymore, but it's still likely? @@ -102,3 +103,26 @@ func (connector *Okta) ListEvents( return rv, streamState, annos, nil } + +// wrapListEventsError annotates errors from the Okta /api/v1/logs endpoint with HTTP +// status, X-Okta-Request-Id, and the query window. The vendored okta-sdk-golang's +// *okta.Error formatter returns the literal string "the API returned an unknown error" +// whenever neither ErrorDescription nor ErrorSummary is populated (HTML 5xx pages, +// gateway timeouts, non-JSON bodies), so without this enrichment the failures are +// undiagnosable from traces alone. +func wrapListEventsError(qp *query.Params, resp *okta.Response, err error) error { + parts := []string{"okta-connectorv2: ListEvents failed"} + if resp != nil && resp.Response != nil { + parts = append(parts, fmt.Sprintf("status=%d", resp.StatusCode)) + if rid := resp.Header.Get("X-Okta-Request-Id"); rid != "" { + parts = append(parts, fmt.Sprintf("request_id=%s", rid)) + } + } + if qp != nil && qp.Since != "" { + parts = append(parts, fmt.Sprintf("since=%s", qp.Since)) + } + if qp != nil && qp.After != "" { + parts = append(parts, fmt.Sprintf("after=%s", qp.After)) + } + return fmt.Errorf("%s: %w", strings.Join(parts, " "), err) +} diff --git a/pkg/connector/event_log_test.go b/pkg/connector/event_log_test.go new file mode 100644 index 000000000..6ffd07a71 --- /dev/null +++ b/pkg/connector/event_log_test.go @@ -0,0 +1,63 @@ +package connector + +import ( + "errors" + "net/http" + "testing" + + "github.com/okta/okta-sdk-golang/v2/okta" + "github.com/okta/okta-sdk-golang/v2/okta/query" + "github.com/stretchr/testify/require" +) + +func TestWrapListEventsError_FullContext(t *testing.T) { + // On an Okta SDK error with a non-nil response, the wrapper must surface HTTP status, + // X-Okta-Request-Id, and the query window — otherwise the inner error reduces to the + // useless "the API returned an unknown error" sentinel from okta-sdk-golang. + header := http.Header{} + header.Set("X-Okta-Request-Id", "req-abc123") + resp := &okta.Response{Response: &http.Response{StatusCode: http.StatusBadGateway, Header: header}} + qp := &query.Params{Since: "2026-05-13T00:00:00Z", After: "0oa1cursor"} + inner := errors.New("the API returned an unknown error") + + wrapped := wrapListEventsError(qp, resp, inner) + require.Error(t, wrapped) + msg := wrapped.Error() + require.Contains(t, msg, "okta-connectorv2: ListEvents failed") + require.Contains(t, msg, "status=502") + require.Contains(t, msg, "request_id=req-abc123") + require.Contains(t, msg, "since=2026-05-13T00:00:00Z") + require.Contains(t, msg, "after=0oa1cursor") + require.ErrorIs(t, wrapped, inner, "wrapper must preserve errors.Is on the inner error") +} + +func TestWrapListEventsError_NilResponse(t *testing.T) { + // On a network failure before any HTTP response is built, the SDK returns nil resp; + // the wrapper must still produce a usable message anchored to the query window. + qp := &query.Params{Since: "2026-05-13T00:00:00Z"} + inner := errors.New("dial tcp: connection refused") + + wrapped := wrapListEventsError(qp, nil, inner) + msg := wrapped.Error() + require.Contains(t, msg, "okta-connectorv2: ListEvents failed") + require.NotContains(t, msg, "status=") + require.NotContains(t, msg, "request_id=") + require.Contains(t, msg, "since=2026-05-13T00:00:00Z") + require.NotContains(t, msg, "after=", "after= must be omitted when not set") + require.ErrorIs(t, wrapped, inner) +} + +func TestWrapListEventsError_MissingRequestIdAndCursor(t *testing.T) { + // Older Okta deployments or gateway-injected error responses sometimes omit the + // request-id header; first-page requests have no cursor. Optional fields must be + // elided cleanly (no empty key=value pairs). + resp := &okta.Response{Response: &http.Response{StatusCode: http.StatusInternalServerError, Header: http.Header{}}} + qp := &query.Params{Since: "2026-05-13T00:00:00Z"} + inner := errors.New("the API returned an unknown error") + + wrapped := wrapListEventsError(qp, resp, inner) + msg := wrapped.Error() + require.Contains(t, msg, "status=500") + require.NotContains(t, msg, "request_id=") + require.NotContains(t, msg, "after=") +}