Skip to content

Commit 91984e3

Browse files
committed
feat(cli): migrate incident feed + insight responder off legacy SDK
Move the last two read commands that were pinned to the hand-written SDK onto go-flashduty: - insight responder: now calls Analytics.ByResponder. Drops the EMAIL column — the backend /insight/responder (RspdIncMetrics) never returns an email, so the legacy SDK's ResponderInsightItem.Email was always blank (a decode-only field). - incident feed: now calls Incidents.Feed. go-flashduty returns raw feed items, so resolveFeedOperators() replicates the legacy operator-name enrichment by resolving each entry's creator_id via Members.PersonInfos (best-effort; falls back to the numeric ID, or 'system' for creator_id 0). Removes the now-dead GetIncidentFeed + QueryInsightByResponder from the flashdutyClient interface and mockClient, and rewrites the feed-empty test to use the gfStub httptest seam. Build/vet/gofmt/test green; both commands live-verified against api-dev (responder lists real data sans EMAIL; feed resolves operator names).
1 parent e3da654 commit 91984e3

4 files changed

Lines changed: 65 additions & 42 deletions

File tree

internal/cli/command_test.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,6 @@ func (m *mockClient) GetIncidentDetail(context.Context, *flashduty.GetIncidentDe
126126
return nil, fmt.Errorf("mockClient: GetIncidentDetail not implemented")
127127
}
128128

129-
func (m *mockClient) GetIncidentFeed(context.Context, *flashduty.GetIncidentFeedInput) (*flashduty.GetIncidentFeedOutput, error) {
130-
return nil, fmt.Errorf("mockClient: GetIncidentFeed not implemented")
131-
}
132-
133129
func (m *mockClient) ListPostMortems(context.Context, *flashduty.ListPostMortemsInput) (*flashduty.ListPostMortemsOutput, error) {
134130
return nil, fmt.Errorf("mockClient: ListPostMortems not implemented")
135131
}
@@ -197,10 +193,6 @@ func (m *mockClient) QueryInsightByChannel(context.Context, *flashduty.InsightQu
197193
return nil, fmt.Errorf("mockClient: QueryInsightByChannel not implemented")
198194
}
199195

200-
func (m *mockClient) QueryInsightByResponder(context.Context, *flashduty.InsightQueryInput) (*flashduty.QueryInsightByResponderOutput, error) {
201-
return nil, fmt.Errorf("mockClient: QueryInsightByResponder not implemented")
202-
}
203-
204196
func (m *mockClient) QueryInsightAlertTopK(context.Context, *flashduty.QueryInsightAlertTopKInput) (*flashduty.QueryInsightAlertTopKOutput, error) {
205197
return nil, fmt.Errorf("mockClient: QueryInsightAlertTopK not implemented")
206198
}
@@ -666,15 +658,10 @@ func TestCommandMemberListPersonInfos(t *testing.T) {
666658
// Regression tests for new command batch review findings
667659
// ---------------------------------------------------------------------------
668660

669-
type mockIncidentFeedEmpty struct{ mockClient }
670-
671-
func (m *mockIncidentFeedEmpty) GetIncidentFeed(_ context.Context, _ *flashduty.GetIncidentFeedInput) (*flashduty.GetIncidentFeedOutput, error) {
672-
return &flashduty.GetIncidentFeedOutput{Items: nil, HasNextPage: false}, nil
673-
}
674-
675661
func TestCommandIncidentFeedEmpty_JSON(t *testing.T) {
676662
saveAndResetGlobals(t)
677-
newClientFn = func() (flashdutyClient, error) { return &mockIncidentFeedEmpty{}, nil }
663+
stub := newGFStub(t)
664+
stub.data = map[string]any{"items": []any{}, "has_next_page": false}
678665

679666
out, err := execCommand("incident", "feed", "inc-1", "--json")
680667
if err != nil {

internal/cli/incident.go

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,12 +1171,11 @@ func newIncidentFeedCmd() *cobra.Command {
11711171
Short: "View incident feed (paginated timeline)",
11721172
Args: requireArgs("incident_id"),
11731173
RunE: func(cmd *cobra.Command, args []string) error {
1174-
return runCommand(cmd, args, func(ctx *RunContext) error {
1175-
result, err := ctx.Client.GetIncidentFeed(cmdContext(ctx.Cmd), &flashduty.GetIncidentFeedInput{
1176-
IncidentID: ctx.Args[0],
1177-
Limit: limit,
1178-
Page: page,
1179-
})
1174+
return runGFCommand(cmd, args, func(ctx *RunContext) error {
1175+
feedReq := &gflashduty.ListIncidentFeedRequest{IncidentID: ctx.Args[0]}
1176+
feedReq.Page = page
1177+
feedReq.Limit = limit
1178+
result, _, err := ctx.GFClient.Incidents.Feed(cmdContext(ctx.Cmd), feedReq)
11801179
if err != nil {
11811180
return err
11821181
}
@@ -1186,12 +1185,27 @@ func newIncidentFeedCmd() *cobra.Command {
11861185
return nil
11871186
}
11881187

1188+
// go-flashduty returns raw feed items, so replicate the legacy
1189+
// SDK's operator-name enrichment by resolving each entry's actor
1190+
// (creator) person ID via /person/infos. Best-effort: the OPERATOR
1191+
// column falls back to the numeric ID when a name can't be resolved.
1192+
nameByID := resolveFeedOperators(ctx, result.Items)
1193+
11891194
cols := []output.Column{
1190-
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.TimelineEvent).Timestamp) }},
1191-
{Header: "TYPE", Field: func(v any) string { return v.(flashduty.TimelineEvent).Type }},
1192-
{Header: "OPERATOR", Field: func(v any) string { return v.(flashduty.TimelineEvent).OperatorName }},
1195+
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(gflashduty.IncidentFeedItem).CreatedAt) }},
1196+
{Header: "TYPE", Field: func(v any) string { return string(v.(gflashduty.IncidentFeedItem).Type) }},
1197+
{Header: "OPERATOR", Field: func(v any) string {
1198+
it := v.(gflashduty.IncidentFeedItem)
1199+
if it.CreatorID == 0 {
1200+
return "system"
1201+
}
1202+
if n, ok := nameByID[it.CreatorID]; ok && n != "" {
1203+
return n
1204+
}
1205+
return strconv.FormatInt(it.CreatorID, 10)
1206+
}},
11931207
{Header: "DETAIL", MaxWidth: 80, Field: func(v any) string {
1194-
d := v.(flashduty.TimelineEvent).Detail
1208+
d := v.(gflashduty.IncidentFeedItem).Detail
11951209
if d == nil {
11961210
return "-"
11971211
}
@@ -1210,6 +1224,37 @@ func newIncidentFeedCmd() *cobra.Command {
12101224
return cmd
12111225
}
12121226

1227+
// resolveFeedOperators resolves the actor (creator) person IDs of incident-feed
1228+
// items to display names via /person/infos, replicating the operator-name
1229+
// enrichment the legacy SDK did server-side. Best-effort: a lookup failure
1230+
// yields a nil map and callers fall back to the numeric ID.
1231+
func resolveFeedOperators(rc *RunContext, items []gflashduty.IncidentFeedItem) map[int64]string {
1232+
seen := make(map[int64]struct{}, len(items))
1233+
ids := make([]uint64, 0, len(items))
1234+
for _, it := range items {
1235+
if it.CreatorID == 0 {
1236+
continue
1237+
}
1238+
if _, ok := seen[it.CreatorID]; ok {
1239+
continue
1240+
}
1241+
seen[it.CreatorID] = struct{}{}
1242+
ids = append(ids, uint64(it.CreatorID))
1243+
}
1244+
if len(ids) == 0 {
1245+
return nil
1246+
}
1247+
resp, _, err := rc.GFClient.Members.PersonInfos(cmdContext(rc.Cmd), &gflashduty.PersonInfosRequest{PersonIDs: ids})
1248+
if err != nil || resp == nil {
1249+
return nil
1250+
}
1251+
out := make(map[int64]string, len(resp.Items))
1252+
for _, p := range resp.Items {
1253+
out[int64(p.PersonID)] = p.PersonName
1254+
}
1255+
return out
1256+
}
1257+
12131258
func newIncidentDetailCmd() *cobra.Command {
12141259
return &cobra.Command{
12151260
Use: "detail <id>",

internal/cli/insight.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,8 @@ func newInsightResponderCmd() *cobra.Command {
156156
cmd := &cobra.Command{
157157
Use: "responder",
158158
Short: "Query insights by responder",
159-
// TODO(go-flashduty migration): not migrated. The EMAIL column reads a
160-
// responder email that the thin go-flashduty ResponderInsightItem does
161-
// not carry (no responder_email field). Migrate once the SDK exposes it
162-
// or the column drops the enriched email. Kept on the legacy SDK.
163159
RunE: func(cmd *cobra.Command, args []string) error {
164-
return runCommand(cmd, args, func(ctx *RunContext) error {
160+
return runGFCommand(cmd, args, func(ctx *RunContext) error {
165161
startTime, err := timeutil.Parse(since)
166162
if err != nil {
167163
return fmt.Errorf("invalid --since: %w", err)
@@ -171,7 +167,7 @@ func newInsightResponderCmd() *cobra.Command {
171167
return fmt.Errorf("invalid --until: %w", err)
172168
}
173169

174-
result, err := ctx.Client.QueryInsightByResponder(cmdContext(ctx.Cmd), &flashduty.InsightQueryInput{
170+
result, _, err := ctx.GFClient.Analytics.ByResponder(cmdContext(ctx.Cmd), &gflashduty.InsightQueryRequest{
175171
StartTime: startTime,
176172
EndTime: endTime,
177173
})
@@ -181,25 +177,22 @@ func newInsightResponderCmd() *cobra.Command {
181177

182178
cols := []output.Column{
183179
{Header: "RESPONDER", MaxWidth: 30, Field: func(v any) string {
184-
return v.(flashduty.ResponderInsightItem).ResponderName
185-
}},
186-
{Header: "EMAIL", MaxWidth: 30, Field: func(v any) string {
187-
return v.(flashduty.ResponderInsightItem).Email
180+
return v.(gflashduty.ResponderInsightItem).ResponderName
188181
}},
189182
{Header: "INCIDENTS", Field: func(v any) string {
190-
return fmt.Sprintf("%d", v.(flashduty.ResponderInsightItem).TotalIncidentCnt)
183+
return fmt.Sprintf("%d", v.(gflashduty.ResponderInsightItem).TotalIncidentCnt)
191184
}},
192185
{Header: "ACK%", Field: func(v any) string {
193-
return fmt.Sprintf("%.0f%%", v.(flashduty.ResponderInsightItem).AcknowledgementPct*100)
186+
return fmt.Sprintf("%.0f%%", v.(gflashduty.ResponderInsightItem).AcknowledgementPct*100)
194187
}},
195188
{Header: "MTTA", Field: func(v any) string {
196-
return output.FormatDurationFloat(v.(flashduty.ResponderInsightItem).MeanSecondsToAck)
189+
return output.FormatDurationFloat(v.(gflashduty.ResponderInsightItem).MeanSecondsToAck)
197190
}},
198191
{Header: "INTERRUPTIONS", Field: func(v any) string {
199-
return fmt.Sprintf("%d", v.(flashduty.ResponderInsightItem).TotalInterruptions)
192+
return fmt.Sprintf("%d", v.(gflashduty.ResponderInsightItem).TotalInterruptions)
200193
}},
201194
{Header: "ENGAGED", Field: func(v any) string {
202-
return output.FormatDuration(v.(flashduty.ResponderInsightItem).TotalEngagedSeconds)
195+
return output.FormatDuration(int(v.(gflashduty.ResponderInsightItem).TotalEngagedSeconds))
203196
}},
204197
}
205198

internal/cli/root.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ type flashdutyClient interface {
5353

5454
// === PHASE 1: Incident additions ===
5555
GetIncidentDetail(ctx context.Context, input *flashduty.GetIncidentDetailInput) (*flashduty.GetIncidentDetailOutput, error)
56-
GetIncidentFeed(ctx context.Context, input *flashduty.GetIncidentFeedInput) (*flashduty.GetIncidentFeedOutput, error)
5756
ListPostMortems(ctx context.Context, input *flashduty.ListPostMortemsInput) (*flashduty.ListPostMortemsOutput, error)
5857
MergeIncidents(ctx context.Context, input *flashduty.MergeIncidentsInput) error
5958
SnoozeIncidents(ctx context.Context, input *flashduty.SnoozeIncidentsInput) error
@@ -76,7 +75,6 @@ type flashdutyClient interface {
7675
// === PHASE 3: Insight + Admin ===
7776
QueryInsightByTeam(ctx context.Context, input *flashduty.InsightQueryInput) (*flashduty.QueryInsightByTeamOutput, error)
7877
QueryInsightByChannel(ctx context.Context, input *flashduty.InsightQueryInput) (*flashduty.QueryInsightByChannelOutput, error)
79-
QueryInsightByResponder(ctx context.Context, input *flashduty.InsightQueryInput) (*flashduty.QueryInsightByResponderOutput, error)
8078
QueryInsightAlertTopK(ctx context.Context, input *flashduty.QueryInsightAlertTopKInput) (*flashduty.QueryInsightAlertTopKOutput, error)
8179
QueryInsightIncidentList(ctx context.Context, input *flashduty.QueryInsightIncidentListInput) (*flashduty.QueryInsightIncidentListOutput, error)
8280
SearchAuditLogs(ctx context.Context, input *flashduty.SearchAuditLogsInput) (*flashduty.SearchAuditLogsOutput, error)

0 commit comments

Comments
 (0)