Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion clients/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions clients/api/slow_operations_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
39 changes: 39 additions & 0 deletions clients/api/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions cmd/resource/invocations.go
Original file line number Diff line number Diff line change
@@ -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")
}
14 changes: 14 additions & 0 deletions cmd/resource/invocations_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
1 change: 1 addition & 0 deletions cmd/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func init() {
Cmd.AddCommand(envCmd)
Cmd.AddCommand(envListCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(invocationsCmd)
Cmd.AddCommand(addCmd)
Cmd.AddCommand(removeCmd)
}
1 change: 1 addition & 0 deletions plugins/major-build/skills/app-builder/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <errorId>` — inspect runtime errors
- `major app errors resolve <errorId>` — 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.
Expand Down
4 changes: 4 additions & 0 deletions plugins/major-build/skills/debug-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <errorId>` 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.
Expand Down
Loading