scout_apm_mcp 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 94c095f06ab792c7e66a4a3d7893a0476308730d1d346f661ba34d52be5b665b
4
+ data.tar.gz: c1823c4b464ab833663a3026ff3f1c1d90de4c6c3b832f57257ce1f495c161e3
5
+ SHA512:
6
+ metadata.gz: adba47ba7ab8d8838e42c236afe334243425f38701da5467e78badd91195ed029dfe4f5c3b308903c67f118ef10778a5e14dbd4dd4f8feb9801a3778da4d20d0
7
+ data.tar.gz: 657457f4239d0531bf7cac76fcd15e5ac9416947fd24d9ce83e38a74c47d0583a41b8194ae526d915ab540c0a3fb56c50cb0c111a22e5f9a23197c725550031a
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.1.0 (2025-11-20)
4
+
5
+ - Initial release
6
+ - ScoutAPM API client with full endpoint coverage (applications, metrics, endpoints, traces, errors, insights, OpenAPI)
7
+ - MCP server integration for Cursor IDE with executable `bundle exec scout_apm_mcp`
8
+ - Helper methods for API key management and URL parsing (`Helpers.get_api_key`, `Helpers.parse_scout_url`, `Helpers.decode_endpoint_id`)
9
+ - Support for environment variables and 1Password integration (via optional `opdotenv` gem)
10
+ - Complete RBS type signatures for all public APIs
11
+ - Comprehensive test suite with RSpec
12
+ - Requires Ruby 3.0 or higher
13
+ - All dependencies use latest compatible versions with pessimistic versioning for security
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
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 all
13
+ 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 THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # scout_apm_mcp
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/scout_apm_mcp.svg)](https://badge.fury.io/rb/scout_apm_mcp) [![Test Status](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/scout_apm_mcp.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/scout_apm_mcp.rb/graph/badge.svg?token=HVWDDNLEO5)](https://codecov.io/gh/amkisko/scout_apm_mcp.rb)
4
+
5
+ Ruby gem providing ScoutAPM API client and MCP (Model Context Protocol) server tools for fetching traces, endpoints, metrics, errors, and insights. Integrates with MCP-compatible clients like Cursor IDE, Claude Desktop, and other MCP-enabled tools.
6
+
7
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
+
9
+ <a href="https://www.kiskolabs.com">
10
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
+ </a>
12
+
13
+ ## Requirements
14
+
15
+ - **Ruby 3.1 or higher** (Ruby 3.0 and earlier are not supported)
16
+
17
+ ## Quick Start
18
+
19
+ 1. In scoutapm create API key under Organization settings: https://scoutapm.com/settings
20
+ 2. In 1Password create an item with the name "Scout APM API" and store the API key in a new field named API_KEY
21
+ 3. Configure your favorite service to use local MCP server, ensure OP_ENV_ENTRY_PATH has correct vault and item names (both are visible in 1Password UI)
22
+
23
+ ### Cursor IDE Configuration
24
+
25
+ For Cursor IDE, create or update `.cursor/mcp.json` in your project:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "scout-apm": {
31
+ "command": "bundle",
32
+ "args": ["exec", "scout_apm_mcp"],
33
+ "env": {
34
+ "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Or if installed globally:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "scout-apm": {
47
+ "command": "scout_apm_mcp",
48
+ "env": {
49
+ "OP_ENV_ENTRY_PATH": "op://Vault Name/Item Name"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Claude Desktop Configuration
57
+
58
+ For Claude Desktop, edit the MCP configuration file:
59
+
60
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
61
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "scout-apm": {
67
+ "command": "bundle",
68
+ "args": ["exec", "scout_apm_mcp"],
69
+ "cwd": "/path/to/your/project"
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Or if installed globally:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "scout-apm": {
81
+ "command": "scout_apm_mcp"
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ **Note**: After updating the configuration, restart Claude Desktop for changes to take effect.
88
+
89
+ ### Security Best Practice
90
+
91
+ Do not store API keys or tokens in MCP configuration files. Instead, use one of these methods:
92
+
93
+ 1. **1Password Integration**: Set `OP_ENV_ENTRY_PATH` environment variable (e.g., `op://Vault/Item`) to automatically load credentials via opdotenv
94
+ 2. **1Password CLI**: The gem will automatically fall back to 1Password CLI if opdotenv is not available
95
+ 3. **Environment Variables**: Set `API_KEY` or `SCOUT_APM_API_KEY` in your shell environment (not recommended for production - use secret vault for in-memory provisioning)
96
+
97
+ The gem will automatically detect and use credentials from your environment or 1Password integration.
98
+
99
+ ### Testing with MCP Inspector
100
+
101
+ You can test the MCP server using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) tool:
102
+
103
+ ```bash
104
+ # Set your 1Password entry path (or use API_KEY/SCOUT_APM_API_KEY)
105
+ export OP_ENV_ENTRY_PATH="op://Vault/Scout APM"
106
+
107
+ # Run the MCP inspector with the server
108
+ npx @modelcontextprotocol/inspector bundle exec scout_apm_mcp
109
+ ```
110
+
111
+ The inspector will:
112
+ 1. Start a proxy server and open a browser interface
113
+ 2. Connect to your MCP server via STDIO
114
+ 3. Allow you to test all available tools interactively
115
+ 4. Display request/response messages and any errors
116
+
117
+ This is useful for:
118
+ - Testing tool functionality before integrating with MCP clients
119
+ - Debugging MCP protocol communication
120
+ - Verifying API key configuration
121
+ - Exploring available tools and their parameters
122
+
123
+ ### Running the MCP Server manually
124
+
125
+ After installation, you can start the MCP server immediately:
126
+
127
+ ```bash
128
+ # With bundler
129
+ gem install scout_apm_mcp && bundle exec scout_apm_mcp
130
+
131
+ # Or if installed globally
132
+ scout_apm_mcp
133
+ ```
134
+
135
+ The server will start and communicate via STDIN/STDOUT using the MCP protocol. Make sure you have your ScoutAPM API key configured (see API Key Management section below).
136
+
137
+ ## Features
138
+
139
+ - **ScoutAPM API Client**: Full-featured client for ScoutAPM REST API
140
+ - **MCP Server Integration**: Ready-to-use MCP server compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
141
+ - **API Key Management**: Supports environment variables and 1Password integration (via optional `opdotenv` gem)
142
+ - **URL Parsing**: Helper methods to parse ScoutAPM URLs and extract IDs
143
+ - **Comprehensive API Coverage**: Supports all ScoutAPM API endpoints (apps, metrics, endpoints, traces, errors, insights)
144
+
145
+ ## Basic Usage
146
+
147
+ ### API Client
148
+
149
+ ```ruby
150
+ require "scout_apm_mcp"
151
+
152
+ # Get API key (from environment or 1Password)
153
+ api_key = ScoutApmMcp::Helpers.get_api_key
154
+
155
+ # Create client
156
+ client = ScoutApmMcp::Client.new(api_key: api_key)
157
+
158
+ # List applications
159
+ apps = client.list_apps
160
+
161
+ # Get application details
162
+ app = client.get_app(123)
163
+
164
+ # List endpoints
165
+ endpoints = client.list_endpoints(123)
166
+
167
+ # Fetch trace
168
+ trace = client.fetch_trace(123, 456)
169
+
170
+ # Get metrics
171
+ metrics = client.get_metric(123, "response_time", from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
172
+
173
+ # List error groups
174
+ errors = client.list_error_groups(123, from: "2025-01-01T00:00:00Z", to: "2025-01-02T00:00:00Z")
175
+
176
+ # Get insights
177
+ insights = client.get_all_insights(123, limit: 20)
178
+ ```
179
+
180
+ ### URL Parsing
181
+
182
+ ```ruby
183
+ # Parse a ScoutAPM trace URL
184
+ url = "https://scoutapm.com/apps/123/endpoints/.../trace/456"
185
+ parsed = ScoutApmMcp::Helpers.parse_scout_url(url)
186
+ # => { app_id: 123, endpoint_id: "...", trace_id: 456, decoded_endpoint: "...", query_params: {...} }
187
+ ```
188
+
189
+ ### API Key Management
190
+
191
+ The gem supports multiple methods for API key retrieval (checked in order):
192
+
193
+ 1. **Direct parameter**: Pass `api_key:` when calling `Helpers.get_api_key`
194
+ 2. **Environment variable**: Set `API_KEY` or `SCOUT_APM_API_KEY`
195
+ 3. **1Password via OP_ENV_ENTRY_PATH**: Set `OP_ENV_ENTRY_PATH` environment variable (e.g., `op://Vault/Item`)
196
+ 4. **1Password via opdotenv**: Automatically loads from 1Password if `opdotenv` gem is available and `op_vault`/`op_item` are provided
197
+ 5. **1Password CLI**: Falls back to direct `op` CLI command
198
+
199
+ ```ruby
200
+ # From environment variable (recommended: use in-memory vault or shell environment)
201
+ # Set API_KEY or SCOUT_APM_API_KEY in your environment
202
+ api_key = ScoutApmMcp::Helpers.get_api_key
203
+
204
+ # From 1Password using OP_ENV_ENTRY_PATH (recommended for 1Password users)
205
+ # Set OP_ENV_ENTRY_PATH in your environment (e.g., op://Vault/Item)
206
+ ENV["OP_ENV_ENTRY_PATH"] = "op://YourVault/YourItem"
207
+ api_key = ScoutApmMcp::Helpers.get_api_key
208
+
209
+ # From 1Password with explicit vault/item (requires opdotenv gem or op CLI)
210
+ api_key = ScoutApmMcp::Helpers.get_api_key(
211
+ op_vault: "YourVault",
212
+ op_item: "Your ScoutAPM API",
213
+ op_field: "API_KEY"
214
+ )
215
+ ```
216
+
217
+ **Security Note**: Never hardcode API keys in your code or configuration files. Always use environment variables, in-memory vaults, or secure credential management systems like 1Password.
218
+
219
+ ## API Methods
220
+
221
+ ### Applications
222
+
223
+ - `list_apps` - List all applications
224
+ - `get_app(app_id)` - Get application details
225
+
226
+ ### Metrics
227
+
228
+ - `list_metrics(app_id)` - List available metric types
229
+ - `get_metric(app_id, metric_type, from:, to:)` - Get time-series metric data
230
+
231
+ ### Endpoints
232
+
233
+ - `list_endpoints(app_id, from:, to:)` - List all endpoints
234
+ - `get_endpoint(app_id, endpoint_id)` - Get endpoint details
235
+ - `get_endpoint_metrics(app_id, endpoint_id, metric_type, from:, to:)` - Get endpoint metrics
236
+ - `list_endpoint_traces(app_id, endpoint_id, from:, to:)` - List endpoint traces
237
+
238
+ ### Traces
239
+
240
+ - `fetch_trace(app_id, trace_id)` - Fetch detailed trace information
241
+
242
+ ### Errors
243
+
244
+ - `list_error_groups(app_id, from:, to:, endpoint:)` - List error groups
245
+ - `get_error_group(app_id, error_id)` - Get error group details
246
+ - `get_error_group_errors(app_id, error_id)` - Get errors within a group
247
+
248
+ ### Insights
249
+
250
+ - `get_all_insights(app_id, limit:)` - Get all insight types
251
+ - `get_insight_by_type(app_id, insight_type, limit:)` - Get specific insight type
252
+ - `get_insights_history(app_id, from:, to:, limit:, pagination_cursor:, pagination_direction:, pagination_page:)` - Get historical insights
253
+ - `get_insights_history_by_type(app_id, insight_type, from:, to:, limit:, pagination_cursor:, pagination_direction:, pagination_page:)` - Get historical insights by type
254
+
255
+ ### OpenAPI Schema
256
+
257
+ - `fetch_openapi_schema` - Fetch the ScoutAPM OpenAPI schema
258
+
259
+ ## MCP Server Integration
260
+
261
+ This gem includes a ready-to-use MCP server that can be run directly:
262
+
263
+ ```bash
264
+ # After installing the gem
265
+ bundle exec scout_apm_mcp
266
+ ```
267
+
268
+ Or if installed globally:
269
+
270
+ ```bash
271
+ gem install scout_apm_mcp
272
+ scout_apm_mcp
273
+ ```
274
+
275
+ The server will communicate via STDIN/STDOUT using the MCP protocol. Configure it in your MCP client (e.g., Cursor IDE, Claude Desktop, or other MCP-enabled tools).
276
+
277
+ ## Error Handling
278
+
279
+ The client raises exceptions for API errors:
280
+
281
+ - `RuntimeError` with message containing "Authentication failed" for 401 Unauthorized
282
+ - `RuntimeError` with message containing "Resource not found" for 404 Not Found
283
+ - `RuntimeError` with message containing "API request failed" for other HTTP errors
284
+
285
+ ## Development
286
+
287
+ ```bash
288
+ # Install dependencies
289
+ bundle install
290
+
291
+ # Run tests
292
+ bundle exec rspec
293
+
294
+ # Run tests across multiple Ruby versions
295
+ bundle exec appraisal install
296
+ bundle exec appraisal rspec
297
+
298
+ # Run linting
299
+ bundle exec standardrb --fix
300
+
301
+ # Validate RBS type signatures
302
+ bundle exec rbs validate
303
+ ```
304
+
305
+ ## Contributing
306
+
307
+ Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/scout_apm_mcp.rb.
308
+
309
+ Contribution policy:
310
+ - New features are not necessarily added to the gem
311
+ - Pull request should have test coverage for affected parts
312
+ - Pull request should have changelog entry
313
+
314
+ Review policy:
315
+ - It might take up to 2 calendar weeks to review and merge critical fixes
316
+ - It might take up to 6 calendar months to review and merge pull request
317
+ - It might take up to 1 calendar year to review an issue
318
+
319
+ ## License
320
+
321
+ The gem is available as open source under the terms of the [MIT License](LICENSE.md).
322
+
data/bin/scout_apm_mcp ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Suppress all output to prevent breaking JSON parsing in MCP clients
5
+ # STDOUT is used for MCP protocol communication (JSON-RPC), so we must not output anything
6
+ # STDERR is also suppressed to prevent any error messages from breaking JSON parsing
7
+ unless ENV["DEBUG"]
8
+ require "stringio"
9
+ # Redirect stderr to StringIO to suppress all error output
10
+ $stderr = StringIO.new
11
+ $stderr.set_encoding("UTF-8")
12
+ end
13
+
14
+ require "scout_apm_mcp"
15
+ require "scout_apm_mcp/server"
16
+
17
+ ScoutApmMcp::Server.start
18
+
@@ -0,0 +1,302 @@
1
+ require "uri"
2
+ require "net/http"
3
+ require "json"
4
+ require "base64"
5
+
6
+ module ScoutApmMcp
7
+ # ScoutAPM API client for making authenticated requests to the ScoutAPM API
8
+ #
9
+ # @example
10
+ # api_key = ScoutApmMcp::Helpers.get_api_key
11
+ # client = ScoutApmMcp::Client.new(api_key: api_key)
12
+ # apps = client.list_apps
13
+ # trace = client.fetch_trace(123, 456)
14
+ class Client
15
+ API_BASE = "https://scoutapm.com/api/v0"
16
+
17
+ # @param api_key [String] ScoutAPM API key
18
+ # @param api_base [String] API base URL (default: https://scoutapm.com/api/v0)
19
+ def initialize(api_key:, api_base: API_BASE)
20
+ @api_key = api_key
21
+ @api_base = api_base
22
+ end
23
+
24
+ # List all applications accessible with the provided API key
25
+ #
26
+ # @return [Hash] API response containing applications list
27
+ def list_apps
28
+ uri = URI("#{@api_base}/apps")
29
+ make_request(uri)
30
+ end
31
+
32
+ # Get application details for a specific application
33
+ #
34
+ # @param app_id [Integer] ScoutAPM application ID
35
+ # @return [Hash] API response containing application details
36
+ def get_app(app_id)
37
+ uri = URI("#{@api_base}/apps/#{app_id}")
38
+ make_request(uri)
39
+ end
40
+
41
+ # List available metric types for an application
42
+ #
43
+ # @param app_id [Integer] ScoutAPM application ID
44
+ # @return [Hash] API response containing available metrics
45
+ def list_metrics(app_id)
46
+ uri = URI("#{@api_base}/apps/#{app_id}/metrics")
47
+ make_request(uri)
48
+ end
49
+
50
+ # Get time-series data for a specific metric type
51
+ #
52
+ # @param app_id [Integer] ScoutAPM application ID
53
+ # @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
54
+ # @param from [String, nil] Start time in ISO 8601 format
55
+ # @param to [String, nil] End time in ISO 8601 format
56
+ # @return [Hash] API response containing metric data
57
+ def get_metric(app_id, metric_type, from: nil, to: nil)
58
+ uri = URI("#{@api_base}/apps/#{app_id}/metrics/#{metric_type}")
59
+ uri.query = build_query_string(from: from, to: to)
60
+ make_request(uri)
61
+ end
62
+
63
+ # List all endpoints for an application
64
+ #
65
+ # @param app_id [Integer] ScoutAPM application ID
66
+ # @param from [String, nil] Start time in ISO 8601 format
67
+ # @param to [String, nil] End time in ISO 8601 format
68
+ # @return [Hash] API response containing endpoints list
69
+ def list_endpoints(app_id, from: nil, to: nil)
70
+ uri = URI("#{@api_base}/apps/#{app_id}/endpoints")
71
+ uri.query = build_query_string(from: from, to: to)
72
+ make_request(uri)
73
+ end
74
+
75
+ # Get endpoint details
76
+ #
77
+ # @param app_id [Integer] ScoutAPM application ID
78
+ # @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
79
+ # @return [Hash] API response containing endpoint details
80
+ def get_endpoint(app_id, endpoint_id)
81
+ encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
82
+ uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}")
83
+ make_request(uri)
84
+ end
85
+
86
+ # Get metric data for a specific endpoint
87
+ #
88
+ # @param app_id [Integer] ScoutAPM application ID
89
+ # @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
90
+ # @param metric_type [String] Metric type (apdex, response_time, response_time_95th, errors, throughput, queue_time)
91
+ # @param from [String, nil] Start time in ISO 8601 format
92
+ # @param to [String, nil] End time in ISO 8601 format
93
+ # @return [Hash] API response containing endpoint metrics
94
+ def get_endpoint_metrics(app_id, endpoint_id, metric_type, from: nil, to: nil)
95
+ encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
96
+ uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/metrics/#{metric_type}")
97
+ uri.query = build_query_string(from: from, to: to)
98
+ make_request(uri)
99
+ end
100
+
101
+ # List traces for a specific endpoint (max 100, within 7 days)
102
+ #
103
+ # @param app_id [Integer] ScoutAPM application ID
104
+ # @param endpoint_id [String] Endpoint ID (base64 URL-encoded)
105
+ # @param from [String, nil] Start time in ISO 8601 format
106
+ # @param to [String, nil] End time in ISO 8601 format
107
+ # @return [Hash] API response containing traces list
108
+ def list_endpoint_traces(app_id, endpoint_id, from: nil, to: nil)
109
+ encoded_endpoint_id = URI.encode_www_form_component(endpoint_id)
110
+ uri = URI("#{@api_base}/apps/#{app_id}/endpoints/#{encoded_endpoint_id}/traces")
111
+ uri.query = build_query_string(from: from, to: to)
112
+ make_request(uri)
113
+ end
114
+
115
+ # Fetch detailed trace information
116
+ #
117
+ # @param app_id [Integer] ScoutAPM application ID
118
+ # @param trace_id [Integer] Trace identifier
119
+ # @return [Hash] API response containing trace details
120
+ def fetch_trace(app_id, trace_id)
121
+ uri = URI("#{@api_base}/apps/#{app_id}/traces/#{trace_id}")
122
+ make_request(uri)
123
+ end
124
+
125
+ # List error groups for an application (max 100, within 30 days)
126
+ #
127
+ # @param app_id [Integer] ScoutAPM application ID
128
+ # @param from [String, nil] Start time in ISO 8601 format
129
+ # @param to [String, nil] End time in ISO 8601 format
130
+ # @param endpoint [String, nil] Base64 URL-encoded endpoint filter (optional)
131
+ # @return [Hash] API response containing error groups list
132
+ def list_error_groups(app_id, from: nil, to: nil, endpoint: nil)
133
+ uri = URI("#{@api_base}/apps/#{app_id}/error_groups")
134
+ params = {}
135
+ params["from"] = from if from
136
+ params["to"] = to if to
137
+ params["endpoint"] = endpoint if endpoint
138
+ uri.query = URI.encode_www_form(params) unless params.empty?
139
+ make_request(uri)
140
+ end
141
+
142
+ # Get details for a specific error group
143
+ #
144
+ # @param app_id [Integer] ScoutAPM application ID
145
+ # @param error_id [Integer] Error group identifier
146
+ # @return [Hash] API response containing error group details
147
+ def get_error_group(app_id, error_id)
148
+ uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}")
149
+ make_request(uri)
150
+ end
151
+
152
+ # Get individual errors within an error group (max 100)
153
+ #
154
+ # @param app_id [Integer] ScoutAPM application ID
155
+ # @param error_id [Integer] Error group identifier
156
+ # @return [Hash] API response containing errors list
157
+ def get_error_group_errors(app_id, error_id)
158
+ uri = URI("#{@api_base}/apps/#{app_id}/error_groups/#{error_id}/errors")
159
+ make_request(uri)
160
+ end
161
+
162
+ # Get all insight types for an application (cached for 5 minutes)
163
+ #
164
+ # @param app_id [Integer] ScoutAPM application ID
165
+ # @param limit [Integer, nil] Maximum number of items per insight type (default: 20)
166
+ # @return [Hash] API response containing all insights
167
+ def get_all_insights(app_id, limit: nil)
168
+ uri = URI("#{@api_base}/apps/#{app_id}/insights")
169
+ uri.query = "limit=#{limit}" if limit
170
+ make_request(uri)
171
+ end
172
+
173
+ # Get data for a specific insight type
174
+ #
175
+ # @param app_id [Integer] ScoutAPM application ID
176
+ # @param insight_type [String] Insight type (n_plus_one, memory_bloat, slow_query)
177
+ # @param limit [Integer, nil] Maximum number of items (default: 20)
178
+ # @return [Hash] API response containing insights
179
+ def get_insight_by_type(app_id, insight_type, limit: nil)
180
+ uri = URI("#{@api_base}/apps/#{app_id}/insights/#{insight_type}")
181
+ uri.query = "limit=#{limit}" if limit
182
+ make_request(uri)
183
+ end
184
+
185
+ # Get historical insights data with cursor-based pagination
186
+ #
187
+ # @param app_id [Integer] ScoutAPM application ID
188
+ # @param from [String, nil] Start time in ISO 8601 format
189
+ # @param to [String, nil] End time in ISO 8601 format
190
+ # @param limit [Integer, nil] Maximum number of items per page (default: 10)
191
+ # @param pagination_cursor [Integer, nil] Cursor for pagination (insight ID)
192
+ # @param pagination_direction [String, nil] Pagination direction (forward, backward)
193
+ # @param pagination_page [Integer, nil] Page number for pagination (default: 1)
194
+ # @return [Hash] API response containing historical insights
195
+ def get_insights_history(app_id, from: nil, to: nil, limit: nil, pagination_cursor: nil, pagination_direction: nil, pagination_page: nil)
196
+ uri = URI("#{@api_base}/apps/#{app_id}/insights/history")
197
+ params = {}
198
+ params["from"] = from if from
199
+ params["to"] = to if to
200
+ params["limit"] = limit if limit
201
+ params["pagination_cursor"] = pagination_cursor if pagination_cursor
202
+ params["pagination_direction"] = pagination_direction if pagination_direction
203
+ params["pagination_page"] = pagination_page if pagination_page
204
+ uri.query = URI.encode_www_form(params) unless params.empty?
205
+ make_request(uri)
206
+ end
207
+
208
+ # Get historical insights data filtered by insight type with cursor-based pagination
209
+ #
210
+ # @param app_id [Integer] ScoutAPM application ID
211
+ # @param insight_type [String] Insight type (n_plus_one, memory_bloat, slow_query)
212
+ # @param from [String, nil] Start time in ISO 8601 format
213
+ # @param to [String, nil] End time in ISO 8601 format
214
+ # @param limit [Integer, nil] Maximum number of items per page (default: 10)
215
+ # @param pagination_cursor [Integer, nil] Cursor for pagination (insight ID)
216
+ # @param pagination_direction [String, nil] Pagination direction (forward, backward)
217
+ # @param pagination_page [Integer, nil] Page number for pagination (default: 1)
218
+ # @return [Hash] API response containing historical insights
219
+ def get_insights_history_by_type(app_id, insight_type, from: nil, to: nil, limit: nil, pagination_cursor: nil, pagination_direction: nil, pagination_page: nil)
220
+ uri = URI("#{@api_base}/apps/#{app_id}/insights/history/#{insight_type}")
221
+ params = {}
222
+ params["from"] = from if from
223
+ params["to"] = to if to
224
+ params["limit"] = limit if limit
225
+ params["pagination_cursor"] = pagination_cursor if pagination_cursor
226
+ params["pagination_direction"] = pagination_direction if pagination_direction
227
+ params["pagination_page"] = pagination_page if pagination_page
228
+ uri.query = URI.encode_www_form(params) unless params.empty?
229
+ make_request(uri)
230
+ end
231
+
232
+ # Fetch the ScoutAPM OpenAPI schema
233
+ #
234
+ # @return [Hash] Hash containing :content, :content_type, and :status
235
+ def fetch_openapi_schema
236
+ uri = URI("https://scoutapm.com/api/v0/openapi.yaml")
237
+ http = build_http_client(uri)
238
+
239
+ request = Net::HTTP::Get.new(uri)
240
+ request["X-SCOUT-API"] = @api_key
241
+ request["Accept"] = "application/x-yaml, application/yaml, text/yaml, */*"
242
+
243
+ response = http.request(request)
244
+
245
+ case response
246
+ when Net::HTTPSuccess
247
+ {
248
+ content: response.body,
249
+ content_type: response.content_type,
250
+ status: response.code.to_i
251
+ }
252
+ when Net::HTTPUnauthorized
253
+ raise "Authentication failed. Check your API key."
254
+ else
255
+ raise "API request failed: #{response.code} #{response.message}"
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ # Build HTTP client
262
+ #
263
+ # @param uri [URI] URI object for the request
264
+ # @return [Net::HTTP] Configured HTTP client
265
+ def build_http_client(uri)
266
+ http = Net::HTTP.new(uri.host, uri.port)
267
+ http.use_ssl = uri.scheme == "https"
268
+ http.read_timeout = 10
269
+ http.open_timeout = 10
270
+ http
271
+ end
272
+
273
+ def build_query_string(from: nil, to: nil)
274
+ params = {}
275
+ params["from"] = from if from
276
+ params["to"] = to if to
277
+ return nil if params.empty?
278
+ URI.encode_www_form(params)
279
+ end
280
+
281
+ def make_request(uri)
282
+ http = build_http_client(uri)
283
+
284
+ request = Net::HTTP::Get.new(uri)
285
+ request["X-SCOUT-API"] = @api_key
286
+ request["Accept"] = "application/json"
287
+
288
+ response = http.request(request)
289
+
290
+ case response
291
+ when Net::HTTPSuccess
292
+ JSON.parse(response.body)
293
+ when Net::HTTPUnauthorized
294
+ raise "Authentication failed. Check your API key. Response: #{response.body}"
295
+ when Net::HTTPNotFound
296
+ raise "Resource not found. Response: #{response.body}"
297
+ else
298
+ raise "API request failed: #{response.code} #{response.message}\n#{response.body}"
299
+ end
300
+ end
301
+ end
302
+ end