diff --git a/clients/api/client.go b/clients/api/client.go index 7746fe1..dae6fb0 100644 --- a/clients/api/client.go +++ b/clients/api/client.go @@ -330,7 +330,6 @@ func (c *Client) SaveApplicationResources(organizationID, applicationID string, return &resp, nil } - // --- Version Check endpoints --- // CheckVersion checks if the CLI version is up to date @@ -648,6 +647,31 @@ func (c *Client) ListAppErrors(applicationID string, req ListAppErrorsRequest) ( return &resp, nil } +// ListSlowOperations lists an app's resource operations ranked by total time spent +func (c *Client) ListSlowOperations(applicationID string, req ListSlowOperationsRequest) (*ListSlowOperationsResponse, error) { + query := url.Values{} + if req.ExecutionEnvironment != "" { + query.Set("executionEnvironment", req.ExecutionEnvironment) + } + if req.WindowDays > 0 { + query.Set("windowDays", fmt.Sprintf("%d", req.WindowDays)) + } + if req.IncludeAll { + query.Set("minP95Ms", "0") + } + + path := fmt.Sprintf("/applications/%s/resource-invocation-aggregates", applicationID) + if encoded := query.Encode(); encoded != "" { + path = path + "?" + encoded + } + + var resp ListSlowOperationsResponse + if err := c.doRequest("GET", path, nil, &resp); err != nil { + return nil, err + } + return &resp, nil +} + // GetAppError retrieves one error's full detail, including its stack trace func (c *Client) GetAppError(applicationID, errorID string) (map[string]any, error) { var resp map[string]any diff --git a/clients/api/slow_operations_test.go b/clients/api/slow_operations_test.go new file mode 100644 index 0000000..f5a00b9 --- /dev/null +++ b/clients/api/slow_operations_test.go @@ -0,0 +1,26 @@ +package api + +import "testing" + +func TestListSlowOperationsUsesInvocationAggregatesRoute(t *testing.T) { + for _, tc := range []struct { + name string + includeAll bool + path string + }{ + {"slow", false, "/applications/app-1/resource-invocation-aggregates?executionEnvironment=coding-session&windowDays=30"}, + {"all", true, "/applications/app-1/resource-invocation-aggregates?executionEnvironment=coding-session&minP95Ms=0&windowDays=30"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, client := newTestServer(t, "GET", tc.path, 200, ListSlowOperationsResponse{Operations: []SlowOperation{}}) + _, err := client.ListSlowOperations("app-1", ListSlowOperationsRequest{ + ExecutionEnvironment: "coding-session", + WindowDays: 30, + IncludeAll: tc.includeAll, + }) + if err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/clients/api/structs.go b/clients/api/structs.go index c433808..8dfad40 100644 --- a/clients/api/structs.go +++ b/clients/api/structs.go @@ -558,6 +558,45 @@ type AppError struct { URL *string `json:"url,omitempty"` } +// ListSlowOperationsRequest filters GET /applications/:applicationId/resource-invocation-aggregates +type ListSlowOperationsRequest struct { + ExecutionEnvironment string + WindowDays int + // IncludeAll disables the server's 500 ms p95 floor. + IncludeAll bool +} + +// SlowOperationCallSite is where the operation lives in the app's source, when the +// query extractor matched the operation key to a call site. +type SlowOperationCallSite struct { + FilePath string `json:"filePath"` + Line *int `json:"line"` + SourceText *string `json:"sourceText"` +} + +// SlowOperation is one aggregated resource operation over the window +type SlowOperation struct { + ResourceID string `json:"resourceId"` + ResourceName string `json:"resourceName"` + ResourceSubtype string `json:"resourceSubtype"` + EnvironmentID *string `json:"environmentId"` + EnvironmentName *string `json:"environmentName"` + OperationKey string `json:"operationKey"` + Calls int `json:"calls"` + P50Ms float64 `json:"p50Ms"` + P95Ms float64 `json:"p95Ms"` + TotalMs float64 `json:"totalMs"` + ErrorRate float64 `json:"errorRate"` + LastSeenAt string `json:"lastSeenAt"` + CallSite *SlowOperationCallSite `json:"callSite"` +} + +// ListSlowOperationsResponse represents GET /applications/:applicationId/resource-invocation-aggregates +type ListSlowOperationsResponse struct { + Error *AppErrorDetail `json:"error,omitempty"` + Operations []SlowOperation `json:"operations"` +} + // ListAppErrorsRequest are the filters for GET /applications/:applicationId/errors type ListAppErrorsRequest struct { Environment string diff --git a/cmd/resource/invocations.go b/cmd/resource/invocations.go new file mode 100644 index 0000000..f0f066b --- /dev/null +++ b/cmd/resource/invocations.go @@ -0,0 +1,48 @@ +package resource + +import ( + "github.com/major-technology/cli/clients/api" + "github.com/major-technology/cli/errors" + "github.com/major-technology/cli/singletons" + "github.com/major-technology/cli/utils" + "github.com/spf13/cobra" +) + +var ( + flagInvocationsEnvironment string + flagInvocationsDays int + flagInvocationsSlow bool +) + +var invocationsCmd = &cobra.Command{ + Use: "invocations", + Short: "List an application's resource invocations, ranked by total time spent", + Long: `Aggregates every resource call the app made over the window by operation +(the invocationKey for typed clients, METHOD host/path for proxy calls) and +reports calls, p50, p95, total time, error rate, and the call site when the +query extractor matched one. Use --slow to show only operations over 500 ms p95. +Sorted by total time descending.`, + RunE: func(cmd *cobra.Command, args []string) error { + appInfo, err := utils.GetApplicationInfo("") + if err != nil { + return errors.WrapError("failed to identify application", err) + } + + resp, err := singletons.GetAPIClient().ListSlowOperations(appInfo.ApplicationID, api.ListSlowOperationsRequest{ + ExecutionEnvironment: flagInvocationsEnvironment, + WindowDays: flagInvocationsDays, + IncludeAll: !flagInvocationsSlow, + }) + if err != nil { + return errors.WrapError("failed to list resource invocations", err) + } + + return utils.WriteJSON(cmd, resp) + }, +} + +func init() { + invocationsCmd.Flags().StringVar(&flagInvocationsEnvironment, "environment", "", "Traffic to include: deployment (default), coding-session, local-dev, or all") + invocationsCmd.Flags().IntVar(&flagInvocationsDays, "days", 0, "Aggregation window in days, 1-30 (server default 7)") + invocationsCmd.Flags().BoolVar(&flagInvocationsSlow, "slow", false, "Only include operations over 500 ms p95") +} diff --git a/cmd/resource/invocations_test.go b/cmd/resource/invocations_test.go new file mode 100644 index 0000000..f494e62 --- /dev/null +++ b/cmd/resource/invocations_test.go @@ -0,0 +1,14 @@ +package resource + +import "testing" + +func TestInvocationsCommand(t *testing.T) { + if invocationsCmd.Parent() != Cmd { + t.Fatal("resource invocations not registered") + } + for _, name := range []string{"environment", "days", "slow"} { + if invocationsCmd.Flags().Lookup(name) == nil { + t.Fatalf("resource invocations missing --%s", name) + } + } +} diff --git a/cmd/resource/resource.go b/cmd/resource/resource.go index 1f6fe3b..d48ff63 100644 --- a/cmd/resource/resource.go +++ b/cmd/resource/resource.go @@ -23,6 +23,7 @@ func init() { Cmd.AddCommand(envCmd) Cmd.AddCommand(envListCmd) Cmd.AddCommand(listCmd) + Cmd.AddCommand(invocationsCmd) Cmd.AddCommand(addCmd) Cmd.AddCommand(removeCmd) } diff --git a/plugins/major-build/skills/app-builder/SKILL.md b/plugins/major-build/skills/app-builder/SKILL.md index b10711e..e9d5485 100644 --- a/plugins/major-build/skills/app-builder/SKILL.md +++ b/plugins/major-build/skills/app-builder/SKILL.md @@ -88,6 +88,7 @@ Run these commands in the mounted app workspace through `mcp__plugin_major-build - `major app logs` — deployed app logs - `major app errors list` / `major app errors get ` — inspect runtime errors - `major app errors resolve ` — after committing a fix for a confirmed runtime error +- `major resource invocations --slow [--environment coding-session] [--days 30]` — resource operations over 500 ms p95, ranked by total time, with call site; omit `--slow` to include faster ones - `major app errors enable` — after adding the Major error-reporter scaffolding to the repo Use each command's `--help` for filters and pagination. diff --git a/plugins/major-build/skills/debug-issue/SKILL.md b/plugins/major-build/skills/debug-issue/SKILL.md index cd4f813..227f96a 100644 --- a/plugins/major-build/skills/debug-issue/SKILL.md +++ b/plugins/major-build/skills/debug-issue/SKILL.md @@ -61,6 +61,10 @@ For blank pages, error overlays, server crashes, failed route handlers, or deplo - After fixing and committing a confirmed app error, use `major app errors resolve ` only when the issue is actually addressed. - If app errors are unavailable and the task is specifically about runtime error monitoring, add the reporter scaffolding before running `major app errors enable`. +## Slow operations + +When the complaint is slowness rather than failure, run `major resource invocations --slow` first. It ranks the app's resource operations by total time with p95 latency and the call site (`callSite.filePath:line`). The `--slow` flag includes only operations over 500 ms p95; omit it to see everything, and use `--environment coding-session` for preview traffic. Open the call site, and for SQL run EXPLAIN through the connector's tools before editing. + ## App logs Use logs when behavior depends on server startup, route handlers, background work, request handling, or errors that are not captured by app errors.