activerabbit-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/exe/activerabbit +6 -0
- data/lib/activerabbit/api_client.rb +306 -0
- data/lib/activerabbit/cli.rb +323 -0
- data/lib/activerabbit/commands.rb +50 -0
- data/lib/activerabbit/config.rb +87 -0
- data/lib/activerabbit/formatters.rb +327 -0
- data/lib/activerabbit/ui.rb +112 -0
- data/lib/activerabbit/version.rb +5 -0
- data/lib/activerabbit.rb +9 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3a37cdfbd12a95561bee8ddd354ef1f2df2c1c913c90a6cc6a9735cb2917c021
|
|
4
|
+
data.tar.gz: 48b2e505b9ef3b2c60444104f8afaf20bfccec8e94414f3f4d4442b32550a11b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0d278067e8c0ae78af59133ba4d2e633f8eef8e2d3e985be1775fc4f74b70020a3725a9ecd921bf54bb63e7e1d58dee30b837e41b39cb7fe2f2958daf2fc02b2
|
|
7
|
+
data.tar.gz: 7b307c5640e79b88f1c20f46b2bf4dd3c5796d9712a7b872fefe90ae8f02aeba20a3382924899533ccd8ca4fa61e0c107d9f46d33ba7f23d8cc11abf68b651a6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to activerabbit-cli will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2025-02-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- `activerabbit login` — authenticate with API key
|
|
10
|
+
- `activerabbit apps` — list available apps
|
|
11
|
+
- `activerabbit use-app <slug>` — set default app
|
|
12
|
+
- `activerabbit status` — app health snapshot
|
|
13
|
+
- `activerabbit incidents` — list incidents with colorized severity and relative time
|
|
14
|
+
- `activerabbit show <id>` — incident details with stack trace
|
|
15
|
+
- `activerabbit explain <id>` — AI-powered root cause analysis
|
|
16
|
+
- `activerabbit trace <endpoint>` — trace analysis with visual span bars
|
|
17
|
+
- `activerabbit deploy-check` — pre-deploy safety check
|
|
18
|
+
- `activerabbit doctor` — verify config and connectivity
|
|
19
|
+
- Multi-app support with interactive selection
|
|
20
|
+
- Output formats: human (default), `--json`, `--md`
|
|
21
|
+
- Config file at `~/.config/activerabbit/config.yml`
|
|
22
|
+
- Environment variable support: `ACTIVERABBIT_API_KEY`, `ACTIVERABBIT_APP`, `ACTIVERABBIT_BASE_URL`
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ActiveRabbit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# activerabbit-cli
|
|
2
|
+
|
|
3
|
+
Production CLI for [ActiveRabbit.ai](https://activerabbit.ai): Rails/JS monitoring and AI-powered issue analysis.
|
|
4
|
+
|
|
5
|
+
Optimized for:
|
|
6
|
+
- **Developers** — fast terminal output with colors, relative times, visual bars
|
|
7
|
+
- **AI agents** — deterministic `--json` output for scripts and automation
|
|
8
|
+
- **CI/CD** — `--md` output for Slack/GitHub comments
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
gem install activerabbit-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This installs the `activerabbit` command.
|
|
17
|
+
|
|
18
|
+
### From source
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/activerabbit/activerabbit-cli.git
|
|
22
|
+
cd activerabbit-cli
|
|
23
|
+
gem build activerabbit-cli.gemspec
|
|
24
|
+
gem install activerabbit-cli-1.0.0.gem
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Login with your API key (from app.activerabbit.ai → Settings → API)
|
|
31
|
+
activerabbit login --api-key YOUR_API_KEY
|
|
32
|
+
|
|
33
|
+
# 2. List your apps
|
|
34
|
+
activerabbit apps
|
|
35
|
+
|
|
36
|
+
# 3. Select default app
|
|
37
|
+
activerabbit use-app jobsgpt-prod
|
|
38
|
+
|
|
39
|
+
# 4. Check status
|
|
40
|
+
activerabbit status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use environment variables:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export ACTIVERABBIT_API_KEY=your_token
|
|
47
|
+
export ACTIVERABBIT_APP=your-app-slug
|
|
48
|
+
activerabbit incidents
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
|--------|-------------|
|
|
55
|
+
| `activerabbit login` | Authenticate with API key |
|
|
56
|
+
| `activerabbit apps` | List available apps |
|
|
57
|
+
| `activerabbit use-app <slug>` | Set default app |
|
|
58
|
+
| `activerabbit status` | App health: errors (24h), p95 latency, deploy status, top issue |
|
|
59
|
+
| `activerabbit incidents [--limit N]` | List incidents with severity, endpoint, count, relative time |
|
|
60
|
+
| `activerabbit show <id>` | Incident details: stack trace, affected users, recent events |
|
|
61
|
+
| `activerabbit explain <id>` | AI analysis: root cause, suggested fix, regression risk, tests |
|
|
62
|
+
| `activerabbit trace <endpoint\|id>` | Trace analysis with visual span bars and bottlenecks |
|
|
63
|
+
| `activerabbit deploy-check` | Pre-deploy safety check |
|
|
64
|
+
| `activerabbit doctor` | Verify config and API connectivity |
|
|
65
|
+
|
|
66
|
+
## Output formats
|
|
67
|
+
|
|
68
|
+
Every command supports three output modes:
|
|
69
|
+
|
|
70
|
+
| Flag | Use case |
|
|
71
|
+
|------|----------|
|
|
72
|
+
| *(default)* | Human-readable with colors, relative times, visual bars |
|
|
73
|
+
| `--json` | Deterministic JSON for scripts, CI, AI agents |
|
|
74
|
+
| `--md` | Markdown for Slack, GitHub comments, reports |
|
|
75
|
+
|
|
76
|
+
**Note:** Global options must come **before** the command:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Correct
|
|
80
|
+
activerabbit --json incidents
|
|
81
|
+
|
|
82
|
+
# Also works (command-specific flags after)
|
|
83
|
+
activerabbit --json incidents --limit 5
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## JSON shape
|
|
87
|
+
|
|
88
|
+
All commands return an envelope like:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"project": "project_id",
|
|
93
|
+
"generated_at": "2025-01-15T12:00:00Z",
|
|
94
|
+
"command": "explain",
|
|
95
|
+
"data": { ... }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Example for `explain`:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"project": "42",
|
|
104
|
+
"generated_at": "2025-01-15T12:00:00Z",
|
|
105
|
+
"command": "explain",
|
|
106
|
+
"data": {
|
|
107
|
+
"incident_id": "inc_123",
|
|
108
|
+
"severity": "high",
|
|
109
|
+
"title": "NoMethodError in JobsController#show",
|
|
110
|
+
"root_cause": "...",
|
|
111
|
+
"confidence_score": 0.87,
|
|
112
|
+
"affected_endpoints": ["/jobs", "/jobs/:id"],
|
|
113
|
+
"suggested_fix": "...",
|
|
114
|
+
"regression_risk": "medium",
|
|
115
|
+
"tests_to_run": ["spec/requests/jobs_spec.rb"],
|
|
116
|
+
"estimated_impact": "..."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## CI / script example
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Fail deploy if project not healthy
|
|
125
|
+
activerabbit status --json | jq -e '.data.health == "ok"' || exit 1
|
|
126
|
+
|
|
127
|
+
# Post incident summary to Slack (e.g. from GitHub Actions)
|
|
128
|
+
activerabbit explain "$INCIDENT_ID" --md > summary.md
|
|
129
|
+
# Then upload summary.md to Slack
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
Config priority (highest to lowest):
|
|
135
|
+
|
|
136
|
+
1. CLI flags (`--app`, `--base-url`)
|
|
137
|
+
2. Environment variables
|
|
138
|
+
3. Config file (`~/.config/activerabbit/config.yml`)
|
|
139
|
+
|
|
140
|
+
| Env var | Description |
|
|
141
|
+
|---------|-------------|
|
|
142
|
+
| `ACTIVERABBIT_API_KEY` | API token (required) |
|
|
143
|
+
| `ACTIVERABBIT_APP` | Default app slug |
|
|
144
|
+
| `ACTIVERABBIT_BASE_URL` | API URL (default: https://app.activerabbit.ai) |
|
|
145
|
+
|
|
146
|
+
## CI/CD example
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
#!/bin/bash
|
|
150
|
+
# Deploy gate: fail if app has critical errors
|
|
151
|
+
|
|
152
|
+
status=$(activerabbit --json status)
|
|
153
|
+
health=$(echo "$status" | jq -r '.data.health')
|
|
154
|
+
|
|
155
|
+
if [ "$health" != "ok" ]; then
|
|
156
|
+
echo "Deploy blocked: app health is $health"
|
|
157
|
+
activerabbit incidents --limit 3
|
|
158
|
+
exit 1
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
echo "App healthy, proceeding with deploy"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Roadmap (v2)
|
|
165
|
+
|
|
166
|
+
- GitHub PR integration — auto-comment with explain/deploy-check
|
|
167
|
+
- Deploy diff — compare errors before/after deploy
|
|
168
|
+
- MCP server — expose CLI to AI agents via Model Context Protocol
|
|
169
|
+
- `activerabbit watch` — stream incidents in real-time
|
|
170
|
+
- Webhook triggers — run CLI commands on new incidents
|
data/exe/activerabbit
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module ActiveRabbit
|
|
9
|
+
# HTTP client for ActiveRabbit API. Uses X-Project-Token (project API key).
|
|
10
|
+
# Designed so backend can add real endpoints (status, incidents, explain, traces) later;
|
|
11
|
+
# until then, uses mock responses when server returns 404 or endpoint is missing.
|
|
12
|
+
class ApiClient
|
|
13
|
+
DEFAULT_BASE_URL = "https://app.activerabbit.ai"
|
|
14
|
+
OPEN_TIMEOUT = 10
|
|
15
|
+
READ_TIMEOUT = 30
|
|
16
|
+
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
class Unauthorized < Error; end
|
|
19
|
+
class NotFound < Error; end
|
|
20
|
+
class RateLimited < Error; end
|
|
21
|
+
class NetworkError < Error; end
|
|
22
|
+
|
|
23
|
+
def initialize(config)
|
|
24
|
+
@config = config
|
|
25
|
+
@base_url = (config.base_url.to_s.strip != "" ? config.base_url : DEFAULT_BASE_URL).to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get(path, query = {})
|
|
29
|
+
uri = build_uri(path, query)
|
|
30
|
+
req = Net::HTTP::Get.new(uri)
|
|
31
|
+
request(uri, req)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def post(path, body = {})
|
|
35
|
+
uri = build_uri(path)
|
|
36
|
+
req = Net::HTTP::Post.new(uri)
|
|
37
|
+
req["Content-Type"] = "application/json"
|
|
38
|
+
req.body = body.is_a?(String) ? body : body.to_json
|
|
39
|
+
request(uri, req)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- API methods (return hashes; mock when backend not ready) ---
|
|
43
|
+
|
|
44
|
+
def list_apps
|
|
45
|
+
get("/api/v1/cli/apps")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def project_status(app = nil)
|
|
49
|
+
id = app || @config.effective_app
|
|
50
|
+
get("/api/v1/cli/apps/#{id}/status")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def incidents(app = nil, limit: 10)
|
|
54
|
+
id = app || @config.effective_app
|
|
55
|
+
get("/api/v1/cli/apps/#{id}/incidents", "limit" => limit)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def incident_detail(incident_id, app = nil)
|
|
59
|
+
id = app || @config.effective_app
|
|
60
|
+
get("/api/v1/cli/apps/#{id}/incidents/#{incident_id}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def explain_incident(incident_id, app = nil)
|
|
64
|
+
id = app || @config.effective_app
|
|
65
|
+
get("/api/v1/cli/apps/#{id}/incidents/#{incident_id}/explain")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def trace(endpoint_or_trace_id, app = nil)
|
|
69
|
+
id = app || @config.effective_app
|
|
70
|
+
if endpoint_or_trace_id.include?("/") && !endpoint_or_trace_id.start_with?("tr_")
|
|
71
|
+
get("/api/v1/cli/apps/#{id}/traces", "endpoint" => endpoint_or_trace_id)
|
|
72
|
+
else
|
|
73
|
+
get("/api/v1/cli/apps/#{id}/traces/#{endpoint_or_trace_id}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deploy_check(app = nil)
|
|
78
|
+
id = app || @config.effective_app
|
|
79
|
+
get("/api/v1/cli/apps/#{id}/deploy_check")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def build_uri(path, query = {})
|
|
85
|
+
full = path.start_with?("http") ? path : "#{@base_url}#{path}"
|
|
86
|
+
uri = URI(full)
|
|
87
|
+
uri.query = URI.encode_www_form(query) if query.any?
|
|
88
|
+
uri
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def request(uri, req)
|
|
92
|
+
req["X-Project-Token"] = @config.api_key
|
|
93
|
+
req["Accept"] = "application/json"
|
|
94
|
+
req["User-Agent"] = "activerabbit-cli/#{ActiveRabbit::VERSION}"
|
|
95
|
+
|
|
96
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
|
|
97
|
+
http.request(req)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
handle_response(uri, res)
|
|
101
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
102
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def handle_response(uri, res)
|
|
106
|
+
body = res.body.to_s.strip
|
|
107
|
+
parsed = (body.empty? ? {} : JSON.parse(body)) rescue {}
|
|
108
|
+
|
|
109
|
+
case res.code.to_i
|
|
110
|
+
when 200, 201
|
|
111
|
+
parsed.is_a?(Hash) ? parsed : { "data" => parsed }
|
|
112
|
+
when 401
|
|
113
|
+
raise Unauthorized, parsed["message"] || "Invalid or missing API key"
|
|
114
|
+
when 404
|
|
115
|
+
# Return mock so CLI works before backend has CLI endpoints
|
|
116
|
+
mock_for(uri, res: res, parsed: parsed)
|
|
117
|
+
when 429
|
|
118
|
+
raise RateLimited, parsed["message"] || "Too many requests"
|
|
119
|
+
when 500..599
|
|
120
|
+
raise Error, parsed["message"] || "Server error (#{res.code})"
|
|
121
|
+
else
|
|
122
|
+
raise Error, parsed["message"] || "Request failed (#{res.code})"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def mock_for(uri, res:, parsed:)
|
|
127
|
+
path = uri.path
|
|
128
|
+
query = URI.decode_www_form(uri.query || "").to_h
|
|
129
|
+
if path =~ %r{/cli/apps$}
|
|
130
|
+
mock_apps
|
|
131
|
+
elsif path.include?("/status")
|
|
132
|
+
mock_status
|
|
133
|
+
elsif path.include?("/incidents") && !path.include?("/explain")
|
|
134
|
+
if path =~ %r{/incidents/([^/]+)$}
|
|
135
|
+
mock_incident_detail($1)
|
|
136
|
+
else
|
|
137
|
+
mock_incidents
|
|
138
|
+
end
|
|
139
|
+
elsif path.include?("/explain")
|
|
140
|
+
mock_explain(path)
|
|
141
|
+
elsif path.include?("/traces")
|
|
142
|
+
mock_trace(path, query)
|
|
143
|
+
elsif path.include?("/deploy_check")
|
|
144
|
+
mock_deploy_check
|
|
145
|
+
else
|
|
146
|
+
raise NotFound, parsed["message"] || "Not found"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def mock_apps
|
|
151
|
+
{
|
|
152
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
153
|
+
"command" => "apps",
|
|
154
|
+
"data" => {
|
|
155
|
+
"apps" => [
|
|
156
|
+
{ "slug" => "jobsgpt-prod", "name" => "JobsGPT Production", "environment" => "production", "error_count_24h" => 12 },
|
|
157
|
+
{ "slug" => "activerabbit-marketing", "name" => "ActiveRabbit Marketing", "environment" => "production", "error_count_24h" => 0 },
|
|
158
|
+
{ "slug" => "client-storefront", "name" => "Client Storefront", "environment" => "staging", "error_count_24h" => 3 }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def mock_status
|
|
165
|
+
app = @config.effective_app || "default"
|
|
166
|
+
{
|
|
167
|
+
"project" => app,
|
|
168
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
169
|
+
"command" => "status",
|
|
170
|
+
"data" => {
|
|
171
|
+
"app" => app,
|
|
172
|
+
"name" => "JobsGPT Production",
|
|
173
|
+
"health" => "ok",
|
|
174
|
+
"error_count_24h" => 12,
|
|
175
|
+
"p95_latency_ms" => 245,
|
|
176
|
+
"deploy_status" => "stable",
|
|
177
|
+
"last_deploy_at" => (Time.now - 3600).utc.iso8601,
|
|
178
|
+
"top_issue" => {
|
|
179
|
+
"id" => "inc_abc123",
|
|
180
|
+
"title" => "NoMethodError in JobsController#show",
|
|
181
|
+
"count" => 8,
|
|
182
|
+
"severity" => "high"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def mock_incidents
|
|
189
|
+
app = @config.effective_app || "default"
|
|
190
|
+
now = Time.now
|
|
191
|
+
{
|
|
192
|
+
"project" => app,
|
|
193
|
+
"generated_at" => now.utc.iso8601,
|
|
194
|
+
"command" => "incidents",
|
|
195
|
+
"data" => {
|
|
196
|
+
"incidents" => [
|
|
197
|
+
{ "id" => "inc_abc123", "severity" => "high", "title" => "NoMethodError in JobsController#show", "endpoint" => "GET /jobs/:id", "count" => 127, "last_seen_at" => now.utc.iso8601, "first_seen_at" => (now - 86400).utc.iso8601, "status" => "open" },
|
|
198
|
+
{ "id" => "inc_def456", "severity" => "medium", "title" => "ActiveRecord::RecordNotFound in UsersController#show", "endpoint" => "GET /users/:id", "count" => 42, "last_seen_at" => (now - 1800).utc.iso8601, "first_seen_at" => (now - 172800).utc.iso8601, "status" => "open" },
|
|
199
|
+
{ "id" => "inc_ghi789", "severity" => "low", "title" => "Timeout::Error in ReportsController#export", "endpoint" => "POST /reports/export", "count" => 8, "last_seen_at" => (now - 7200).utc.iso8601, "first_seen_at" => (now - 259200).utc.iso8601, "status" => "wip" },
|
|
200
|
+
{ "id" => "inc_jkl012", "severity" => "critical", "title" => "PG::ConnectionBad in ApplicationController", "endpoint" => "GET /dashboard", "count" => 3, "last_seen_at" => (now - 300).utc.iso8601, "first_seen_at" => (now - 600).utc.iso8601, "status" => "open" }
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def mock_incident_detail(incident_id)
|
|
207
|
+
app = @config.effective_app || "default"
|
|
208
|
+
now = Time.now
|
|
209
|
+
{
|
|
210
|
+
"project" => app,
|
|
211
|
+
"generated_at" => now.utc.iso8601,
|
|
212
|
+
"command" => "incident_detail",
|
|
213
|
+
"data" => {
|
|
214
|
+
"id" => incident_id,
|
|
215
|
+
"severity" => "high",
|
|
216
|
+
"status" => "open",
|
|
217
|
+
"title" => "NoMethodError in JobsController#show",
|
|
218
|
+
"exception_class" => "NoMethodError",
|
|
219
|
+
"message" => "undefined method `status' for nil:NilClass",
|
|
220
|
+
"endpoint" => "GET /jobs/:id",
|
|
221
|
+
"count" => 127,
|
|
222
|
+
"affected_users" => 45,
|
|
223
|
+
"first_seen_at" => (now - 86400).utc.iso8601,
|
|
224
|
+
"last_seen_at" => now.utc.iso8601,
|
|
225
|
+
"top_frame" => "app/controllers/jobs_controller.rb:42:in `show'",
|
|
226
|
+
"backtrace" => [
|
|
227
|
+
"app/controllers/jobs_controller.rb:42:in `show'",
|
|
228
|
+
"app/controllers/application_controller.rb:15:in `wrap_request'",
|
|
229
|
+
"config/initializers/activerabbit.rb:8:in `block in <main>'"
|
|
230
|
+
],
|
|
231
|
+
"recent_events" => [
|
|
232
|
+
{ "at" => now.utc.iso8601, "user_id" => "user_123", "request_id" => "req_abc" },
|
|
233
|
+
{ "at" => (now - 60).utc.iso8601, "user_id" => "user_456", "request_id" => "req_def" },
|
|
234
|
+
{ "at" => (now - 180).utc.iso8601, "user_id" => "user_789", "request_id" => "req_ghi" }
|
|
235
|
+
],
|
|
236
|
+
"tags" => { "controller" => "JobsController", "action" => "show", "environment" => "production" }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def mock_explain(path)
|
|
242
|
+
parts = path.split("/")
|
|
243
|
+
idx = parts.index("incidents")
|
|
244
|
+
incident_id = idx ? parts[idx + 1] : "inc_unknown"
|
|
245
|
+
{
|
|
246
|
+
"project" => @config.project_id || "default",
|
|
247
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
248
|
+
"command" => "explain",
|
|
249
|
+
"data" => {
|
|
250
|
+
"incident_id" => incident_id,
|
|
251
|
+
"severity" => "high",
|
|
252
|
+
"title" => "NoMethodError in JobsController#show",
|
|
253
|
+
"root_cause" => "Call to #status on nil when job was deleted or ID invalid.",
|
|
254
|
+
"confidence_score" => 0.87,
|
|
255
|
+
"affected_endpoints" => ["/jobs", "/jobs/:id"],
|
|
256
|
+
"suggested_fix" => "Add a guard: return head :not_found unless @job",
|
|
257
|
+
"regression_risk" => "medium",
|
|
258
|
+
"tests_to_run" => ["spec/requests/jobs_spec.rb", "spec/controllers/jobs_controller_spec.rb"],
|
|
259
|
+
"estimated_impact" => "Low; fix is localized to one action."
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def mock_trace(path, query = {})
|
|
265
|
+
path_id = path.split("/").last
|
|
266
|
+
endpoint = query["endpoint"]
|
|
267
|
+
if endpoint.to_s.strip != ""
|
|
268
|
+
trace_id = "tr_mock"
|
|
269
|
+
else
|
|
270
|
+
trace_id = path_id
|
|
271
|
+
endpoint = path_id.start_with?("tr_") ? "/jobs" : path_id
|
|
272
|
+
end
|
|
273
|
+
endpoint = "/jobs" if endpoint.to_s.strip == ""
|
|
274
|
+
{
|
|
275
|
+
"project" => @config.project_id || "default",
|
|
276
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
277
|
+
"command" => "trace",
|
|
278
|
+
"data" => {
|
|
279
|
+
"trace_id" => trace_id,
|
|
280
|
+
"endpoint" => endpoint,
|
|
281
|
+
"duration_ms" => 1250,
|
|
282
|
+
"spans" => [
|
|
283
|
+
{ "name" => "JobsController#index", "duration_ms" => 1200, "percent" => 96 },
|
|
284
|
+
{ "name" => "Job.load_all", "duration_ms" => 980, "percent" => 78 },
|
|
285
|
+
{ "name" => "N+1 detected (job.user)", "duration_ms" => 450, "percent" => 36 }
|
|
286
|
+
],
|
|
287
|
+
"bottlenecks" => ["N+1 on job.user", "Heavy serialization in index.json.jbuilder"]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def mock_deploy_check
|
|
293
|
+
{
|
|
294
|
+
"project" => @config.project_id || "default",
|
|
295
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
296
|
+
"command" => "deploy_check",
|
|
297
|
+
"data" => {
|
|
298
|
+
"ready" => true,
|
|
299
|
+
"last_deploy_at" => (Time.now - 3600).utc.iso8601,
|
|
300
|
+
"new_errors_since_deploy" => 0,
|
|
301
|
+
"warnings" => []
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|