- Add universal pagination guard with session-isolated cursor management - Implement withPagination() decorator for any tool returning large datasets - Update browser_console_messages with pagination and advanced filtering - Update browser_get_requests with pagination while preserving all filters - Add adaptive chunk sizing for optimal performance (target 500ms responses) - Include query consistency validation to handle parameter changes - Provide smart response size detection with user recommendations - Add automatic cursor cleanup and 24-hour expiration - Create comprehensive documentation and usage examples Resolves: Large MCP response token overflow warnings Benefits: Predictable response sizes, resumable data exploration, universal UX
559 lines
22 KiB
TypeScript
559 lines
22 KiB
TypeScript
/**
|
||
* Copyright (c) Microsoft Corporation.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
import { z } from 'zod';
|
||
import { defineTool } from './tool.js';
|
||
import { paginationParamsSchema, withPagination } from '../pagination.js';
|
||
import { RequestInterceptorOptions } from '../requestInterceptor.js';
|
||
import type { Context } from '../context.js';
|
||
|
||
const startMonitoringSchema = z.object({
|
||
urlFilter: z.union([
|
||
z.string(),
|
||
z.object({
|
||
type: z.enum(['regex', 'function']),
|
||
value: z.string()
|
||
})
|
||
]).optional().describe('Filter URLs to capture. Can be a string (contains match), regex pattern, or custom function. Examples: "/api/", ".*\\.json$", or custom logic'),
|
||
|
||
captureBody: z.boolean().optional().default(true).describe('Whether to capture request and response bodies (default: true)'),
|
||
|
||
maxBodySize: z.number().optional().default(10485760).describe('Maximum body size to capture in bytes (default: 10MB). Larger bodies will be truncated'),
|
||
|
||
autoSave: z.boolean().optional().default(false).describe('Automatically save captured requests after each response (default: false for performance)'),
|
||
|
||
outputPath: z.string().optional().describe('Custom output directory path. If not specified, uses session artifact directory')
|
||
});
|
||
|
||
const getRequestsSchema = paginationParamsSchema.extend({
|
||
filter: z.enum(['all', 'failed', 'slow', 'errors', 'success']).optional().default('all').describe('Filter requests by type: all, failed (network failures), slow (>1s), errors (4xx/5xx), success (2xx/3xx)'),
|
||
|
||
domain: z.string().optional().describe('Filter requests by domain hostname'),
|
||
|
||
method: z.string().optional().describe('Filter requests by HTTP method (GET, POST, etc.)'),
|
||
|
||
status: z.number().optional().describe('Filter requests by HTTP status code'),
|
||
|
||
format: z.enum(['summary', 'detailed', 'stats']).optional().default('summary').describe('Response format: summary (basic info), detailed (full data), stats (statistics only)'),
|
||
|
||
slowThreshold: z.number().optional().default(1000).describe('Threshold in milliseconds for considering requests "slow" (default: 1000ms)')
|
||
});
|
||
|
||
const exportRequestsSchema = z.object({
|
||
format: z.enum(['json', 'har', 'csv', 'summary']).optional().default('json').describe('Export format: json (full data), har (HTTP Archive), csv (spreadsheet), summary (human-readable report)'),
|
||
|
||
filename: z.string().optional().describe('Custom filename for export. Auto-generated if not specified with timestamp'),
|
||
|
||
filter: z.enum(['all', 'failed', 'slow', 'errors', 'success']).optional().default('all').describe('Filter which requests to export'),
|
||
|
||
includeBody: z.boolean().optional().default(false).describe('Include request/response bodies in export (warning: may create large files)')
|
||
});
|
||
|
||
/**
|
||
* Start comprehensive request monitoring and interception
|
||
*
|
||
* This tool enables deep HTTP traffic analysis during browser automation.
|
||
* Perfect for API reverse engineering, security testing, and performance analysis.
|
||
*
|
||
* Use Cases:
|
||
* - Security testing: Capture all API calls for vulnerability analysis
|
||
* - Performance monitoring: Identify slow endpoints and optimize
|
||
* - API documentation: Generate comprehensive API usage reports
|
||
* - Debugging: Analyze failed requests and error patterns
|
||
*/
|
||
const startRequestMonitoring = defineTool({
|
||
capability: 'core',
|
||
|
||
schema: {
|
||
name: 'browser_start_request_monitoring',
|
||
title: 'Start request monitoring',
|
||
description: 'Enable comprehensive HTTP request/response interception and analysis. Captures headers, bodies, timing, and failure information for all browser traffic. Essential for security testing, API analysis, and performance debugging.',
|
||
inputSchema: startMonitoringSchema,
|
||
type: 'destructive',
|
||
},
|
||
|
||
handle: async (context: Context, params: z.output<typeof startMonitoringSchema>, response) => {
|
||
try {
|
||
await context.ensureTab();
|
||
|
||
// Parse URL filter
|
||
let urlFilter: RequestInterceptorOptions['urlFilter'];
|
||
if (params.urlFilter) {
|
||
if (typeof params.urlFilter === 'string') {
|
||
urlFilter = params.urlFilter;
|
||
} else {
|
||
// Handle regex or function
|
||
if (params.urlFilter.type === 'regex') {
|
||
urlFilter = new RegExp(params.urlFilter.value);
|
||
} else {
|
||
// Function - evaluate safely
|
||
try {
|
||
|
||
urlFilter = eval(`(${params.urlFilter.value})`);
|
||
} catch (error: any) {
|
||
throw new Error(`Invalid filter function: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Get output path from artifact manager or use default
|
||
let outputPath = params.outputPath;
|
||
if (!outputPath && context.sessionId) {
|
||
const artifactManager = context.getArtifactManager();
|
||
if (artifactManager)
|
||
outputPath = artifactManager.getSubdirectory('requests');
|
||
|
||
}
|
||
if (!outputPath)
|
||
outputPath = context.config.outputDir + '/requests';
|
||
|
||
|
||
const options: RequestInterceptorOptions = {
|
||
urlFilter,
|
||
captureBody: params.captureBody,
|
||
maxBodySize: params.maxBodySize,
|
||
autoSave: params.autoSave,
|
||
outputPath
|
||
};
|
||
|
||
// Start monitoring
|
||
await context.startRequestMonitoring(options);
|
||
|
||
response.addResult('✅ **Request monitoring started successfully**');
|
||
response.addResult('');
|
||
response.addResult('📊 **Configuration:**');
|
||
response.addResult(`• URL Filter: ${params.urlFilter || 'All requests'}`);
|
||
response.addResult(`• Capture Bodies: ${params.captureBody ? 'Yes' : 'No'}`);
|
||
response.addResult(`• Max Body Size: ${(params.maxBodySize! / 1024 / 1024).toFixed(1)}MB`);
|
||
response.addResult(`• Auto Save: ${params.autoSave ? 'Yes' : 'No'}`);
|
||
response.addResult(`• Output Path: ${outputPath}`);
|
||
response.addResult('');
|
||
response.addResult('🎯 **Next Steps:**');
|
||
response.addResult('1. Navigate to pages and interact with the application');
|
||
response.addResult('2. Use `browser_get_requests` to view captured traffic');
|
||
response.addResult('3. Use `browser_export_requests` to save analysis results');
|
||
response.addResult('4. Use `browser_clear_requests` to clear captured data');
|
||
|
||
} catch (error: any) {
|
||
throw new Error(`Failed to start request monitoring: ${error.message}`);
|
||
}
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Retrieve and analyze captured HTTP requests
|
||
*
|
||
* Access comprehensive request data including timing, headers, bodies,
|
||
* and failure information. Supports advanced filtering and analysis.
|
||
*/
|
||
const getRequests = defineTool({
|
||
capability: 'core',
|
||
|
||
schema: {
|
||
name: 'browser_get_requests',
|
||
title: 'Get captured requests',
|
||
description: 'Retrieve and analyze captured HTTP requests with pagination support. Shows timing, status codes, headers, and bodies. Large request lists are automatically paginated for better performance.',
|
||
inputSchema: getRequestsSchema,
|
||
type: 'readOnly',
|
||
},
|
||
|
||
handle: async (context: Context, params: z.output<typeof getRequestsSchema>, response) => {
|
||
try {
|
||
const interceptor = context.getRequestInterceptor();
|
||
if (!interceptor) {
|
||
response.addResult('❌ **Request monitoring not active**');
|
||
response.addResult('');
|
||
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
|
||
return;
|
||
}
|
||
|
||
// Special case for stats format - no pagination needed
|
||
if (params.format === 'stats') {
|
||
const stats = interceptor.getStats();
|
||
response.addResult('📊 **Request Statistics**');
|
||
response.addResult('');
|
||
response.addResult(`• Total Requests: ${stats.totalRequests}`);
|
||
response.addResult(`• Successful: ${stats.successfulRequests} (${((stats.successfulRequests / stats.totalRequests) * 100).toFixed(1)}%)`);
|
||
response.addResult(`• Failed: ${stats.failedRequests} (${((stats.failedRequests / stats.totalRequests) * 100).toFixed(1)}%)`);
|
||
response.addResult(`• Errors: ${stats.errorResponses} (${((stats.errorResponses / stats.totalRequests) * 100).toFixed(1)}%)`);
|
||
response.addResult(`• Average Response Time: ${stats.averageResponseTime}ms`);
|
||
response.addResult(`• Slow Requests (>1s): ${stats.slowRequests}`);
|
||
response.addResult('');
|
||
response.addResult('**By Method:**');
|
||
Object.entries(stats.requestsByMethod).forEach(([method, count]) => {
|
||
response.addResult(` • ${method}: ${count}`);
|
||
});
|
||
response.addResult('');
|
||
response.addResult('**By Status Code:**');
|
||
Object.entries(stats.requestsByStatus).forEach(([status, count]) => {
|
||
response.addResult(` • ${status}: ${count}`);
|
||
});
|
||
response.addResult('');
|
||
response.addResult('**Top Domains:**');
|
||
const topDomains = Object.entries(stats.requestsByDomain)
|
||
.sort(([, a], [, b]) => b - a)
|
||
.slice(0, 5);
|
||
topDomains.forEach(([domain, count]) => {
|
||
response.addResult(` • ${domain}: ${count}`);
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Use pagination for request data
|
||
await withPagination(
|
||
'browser_get_requests',
|
||
params,
|
||
context,
|
||
response,
|
||
{
|
||
maxResponseTokens: 8000,
|
||
defaultPageSize: 25, // Smaller default for detailed request data
|
||
dataExtractor: async () => {
|
||
let requests = interceptor.getData();
|
||
|
||
// Apply filters
|
||
if (params.filter !== 'all') {
|
||
switch (params.filter) {
|
||
case 'failed':
|
||
requests = interceptor.getFailedRequests();
|
||
break;
|
||
case 'slow':
|
||
requests = interceptor.getSlowRequests(params.slowThreshold);
|
||
break;
|
||
case 'errors':
|
||
requests = requests.filter(r => r.response && r.response.status >= 400);
|
||
break;
|
||
case 'success':
|
||
requests = requests.filter(r => r.response && r.response.status < 400);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (params.domain) {
|
||
requests = requests.filter(r => {
|
||
try {
|
||
return new URL(r.url).hostname === params.domain;
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (params.method)
|
||
requests = requests.filter(r => r.method.toLowerCase() === params.method!.toLowerCase());
|
||
|
||
if (params.status)
|
||
requests = requests.filter(r => r.response?.status === params.status);
|
||
|
||
return requests;
|
||
},
|
||
itemFormatter: (req, format) => {
|
||
const duration = req.duration ? `${req.duration}ms` : 'pending';
|
||
const status = req.failed ? 'FAILED' : req.response?.status || 'pending';
|
||
const size = req.response?.bodySize ? ` (${(req.response.bodySize / 1024).toFixed(1)}KB)` : '';
|
||
|
||
let result = `**${req.method} ${status}** - ${duration}\n ${req.url}${size}`;
|
||
|
||
if (format === 'detailed') {
|
||
result += `\n 📅 ${req.timestamp}`;
|
||
if (req.response) {
|
||
result += `\n 📊 Status: ${req.response.status} ${req.response.statusText}`;
|
||
result += `\n ⏱️ Duration: ${req.response.duration}ms`;
|
||
result += `\n 🔄 From Cache: ${req.response.fromCache ? 'Yes' : 'No'}`;
|
||
|
||
// Show key headers
|
||
const contentType = req.response.headers['content-type'];
|
||
if (contentType)
|
||
result += `\n 📄 Content-Type: ${contentType}`;
|
||
}
|
||
|
||
if (req.failed && req.failure)
|
||
result += `\n ❌ Failure: ${req.failure.errorText}`;
|
||
|
||
result += '\n';
|
||
}
|
||
|
||
return result;
|
||
},
|
||
sessionIdExtractor: () => context.sessionId,
|
||
positionCalculator: (items, lastIndex) => ({
|
||
lastIndex,
|
||
totalItems: items.length,
|
||
timestamp: Date.now()
|
||
})
|
||
}
|
||
);
|
||
|
||
} catch (error: any) {
|
||
throw new Error(`Failed to get requests: ${error.message}`);
|
||
}
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Export captured requests to various formats for external analysis
|
||
*/
|
||
const exportRequests = defineTool({
|
||
capability: 'core',
|
||
|
||
schema: {
|
||
name: 'browser_export_requests',
|
||
title: 'Export captured requests',
|
||
description: 'Export captured HTTP requests to various formats (JSON, HAR, CSV, or summary report). Perfect for sharing analysis results, importing into other tools, or creating audit reports.',
|
||
inputSchema: exportRequestsSchema,
|
||
type: 'readOnly',
|
||
},
|
||
|
||
handle: async (context: Context, params: z.output<typeof exportRequestsSchema>, response) => {
|
||
try {
|
||
const interceptor = context.getRequestInterceptor();
|
||
if (!interceptor) {
|
||
response.addResult('❌ **Request monitoring not active**');
|
||
response.addResult('');
|
||
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
|
||
return;
|
||
}
|
||
|
||
const requests = interceptor.getData();
|
||
if (requests.length === 0) {
|
||
response.addResult('ℹ️ **No requests to export**');
|
||
response.addResult('');
|
||
response.addResult('💡 Navigate to pages and interact with the application first');
|
||
return;
|
||
}
|
||
|
||
let exportPath: string;
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const defaultFilename = `requests-${timestamp}`;
|
||
|
||
switch (params.format) {
|
||
case 'har':
|
||
exportPath = await interceptor.exportHAR(params.filename || `${defaultFilename}.har`);
|
||
break;
|
||
|
||
case 'json':
|
||
exportPath = await interceptor.save(params.filename || `${defaultFilename}.json`);
|
||
break;
|
||
|
||
case 'csv':
|
||
// Create CSV export
|
||
const csvData = requests.map(req => ({
|
||
timestamp: req.timestamp,
|
||
method: req.method,
|
||
url: req.url,
|
||
status: req.response?.status || (req.failed ? 'FAILED' : 'PENDING'),
|
||
duration: req.duration || '',
|
||
size: req.response?.bodySize || '',
|
||
contentType: req.response?.headers['content-type'] || '',
|
||
fromCache: req.response?.fromCache || false
|
||
}));
|
||
|
||
const csvHeaders = Object.keys(csvData[0]).join(',');
|
||
const csvRows = csvData.map(row => Object.values(row).join(','));
|
||
const csvContent = [csvHeaders, ...csvRows].join('\n');
|
||
|
||
const csvFilename = params.filename || `${defaultFilename}.csv`;
|
||
const csvPath = `${interceptor.getStatus().options.outputPath}/${csvFilename}`;
|
||
await require('fs/promises').writeFile(csvPath, csvContent);
|
||
exportPath = csvPath;
|
||
break;
|
||
|
||
case 'summary':
|
||
// Create human-readable summary
|
||
const stats = interceptor.getStats();
|
||
const summaryLines = [
|
||
'# HTTP Request Analysis Summary',
|
||
`Generated: ${new Date().toISOString()}`,
|
||
'',
|
||
'## Overview',
|
||
`- Total Requests: ${stats.totalRequests}`,
|
||
`- Successful: ${stats.successfulRequests}`,
|
||
`- Failed: ${stats.failedRequests}`,
|
||
`- Errors: ${stats.errorResponses}`,
|
||
`- Average Response Time: ${stats.averageResponseTime}ms`,
|
||
'',
|
||
'## Performance',
|
||
`- Fast Requests (<1s): ${stats.fastRequests}`,
|
||
`- Slow Requests (>1s): ${stats.slowRequests}`,
|
||
'',
|
||
'## Request Methods',
|
||
...Object.entries(stats.requestsByMethod).map(([method, count]) => `- ${method}: ${count}`),
|
||
'',
|
||
'## Status Codes',
|
||
...Object.entries(stats.requestsByStatus).map(([status, count]) => `- ${status}: ${count}`),
|
||
'',
|
||
'## Top Domains',
|
||
...Object.entries(stats.requestsByDomain)
|
||
.sort(([, a], [, b]) => b - a)
|
||
.slice(0, 10)
|
||
.map(([domain, count]) => `- ${domain}: ${count}`),
|
||
'',
|
||
'## Slow Requests (>1s)',
|
||
...interceptor.getSlowRequests().map(req =>
|
||
`- ${req.method} ${req.url} (${req.duration}ms)`
|
||
),
|
||
'',
|
||
'## Failed Requests',
|
||
...interceptor.getFailedRequests().map(req =>
|
||
`- ${req.method} ${req.url} (${req.response?.status || 'NETWORK_FAILED'})`
|
||
)
|
||
];
|
||
|
||
const summaryFilename = params.filename || `${defaultFilename}-summary.md`;
|
||
const summaryPath = `${interceptor.getStatus().options.outputPath}/${summaryFilename}`;
|
||
await require('fs/promises').writeFile(summaryPath, summaryLines.join('\n'));
|
||
exportPath = summaryPath;
|
||
break;
|
||
|
||
default:
|
||
throw new Error(`Unsupported export format: ${params.format}`);
|
||
}
|
||
|
||
response.addResult('✅ **Export completed successfully**');
|
||
response.addResult('');
|
||
response.addResult(`📁 **File saved:** ${exportPath}`);
|
||
response.addResult(`📊 **Exported:** ${requests.length} requests`);
|
||
response.addResult(`🗂️ **Format:** ${params.format.toUpperCase()}`);
|
||
response.addResult('');
|
||
|
||
if (params.format === 'har') {
|
||
response.addResult('💡 **HAR files** can be imported into:');
|
||
response.addResult(' • Chrome DevTools (Network tab)');
|
||
response.addResult(' • Insomnia or Postman');
|
||
response.addResult(' • Online HAR viewers');
|
||
} else if (params.format === 'json') {
|
||
response.addResult('💡 **JSON files** contain full request/response data');
|
||
response.addResult(' • Perfect for programmatic analysis');
|
||
response.addResult(' • Includes headers, bodies, timing info');
|
||
}
|
||
|
||
} catch (error: any) {
|
||
throw new Error(`Failed to export requests: ${error.message}`);
|
||
}
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Clear all captured request data from memory
|
||
*/
|
||
const clearRequests = defineTool({
|
||
capability: 'core',
|
||
|
||
schema: {
|
||
name: 'browser_clear_requests',
|
||
title: 'Clear captured requests',
|
||
description: 'Clear all captured HTTP request data from memory. Useful for freeing up memory during long sessions or when starting fresh analysis.',
|
||
inputSchema: z.object({}),
|
||
type: 'destructive',
|
||
},
|
||
|
||
handle: async (context: Context, params, response) => {
|
||
try {
|
||
const interceptor = context.getRequestInterceptor();
|
||
if (!interceptor) {
|
||
response.addResult('ℹ️ **Request monitoring not active**');
|
||
response.addResult('');
|
||
response.addResult('💡 Start monitoring first with `browser_start_request_monitoring`');
|
||
return;
|
||
}
|
||
|
||
const clearedCount = interceptor.clear();
|
||
|
||
response.addResult('✅ **Request data cleared successfully**');
|
||
response.addResult('');
|
||
response.addResult(`🗑️ **Cleared:** ${clearedCount} captured requests`);
|
||
response.addResult('');
|
||
response.addResult('💡 **Memory freed** - Ready for new monitoring session');
|
||
|
||
} catch (error: any) {
|
||
throw new Error(`Failed to clear requests: ${error.message}`);
|
||
}
|
||
},
|
||
});
|
||
|
||
/**
|
||
* Get current request monitoring status and configuration
|
||
*/
|
||
const getMonitoringStatus = defineTool({
|
||
capability: 'core',
|
||
|
||
schema: {
|
||
name: 'browser_request_monitoring_status',
|
||
title: 'Get request monitoring status',
|
||
description: 'Check if request monitoring is active and view current configuration. Shows capture statistics, filter settings, and output paths.',
|
||
inputSchema: z.object({}),
|
||
type: 'readOnly',
|
||
},
|
||
|
||
handle: async (context: Context, params, response) => {
|
||
try {
|
||
const interceptor = context.getRequestInterceptor();
|
||
|
||
if (!interceptor) {
|
||
response.addResult('❌ **Request monitoring is not active**');
|
||
response.addResult('');
|
||
response.addResult('💡 **To start monitoring:**');
|
||
response.addResult('1. Use `browser_start_request_monitoring` to enable');
|
||
response.addResult('2. Navigate to pages and perform actions');
|
||
response.addResult('3. Use `browser_get_requests` to view captured data');
|
||
return;
|
||
}
|
||
|
||
const status = interceptor.getStatus();
|
||
const stats = interceptor.getStats();
|
||
|
||
response.addResult('✅ **Request monitoring is active**');
|
||
response.addResult('');
|
||
response.addResult('📊 **Current Statistics:**');
|
||
response.addResult(`• Total Captured: ${stats.totalRequests} requests`);
|
||
response.addResult(`• Successful: ${stats.successfulRequests}`);
|
||
response.addResult(`• Failed: ${stats.failedRequests}`);
|
||
response.addResult(`• Average Response Time: ${stats.averageResponseTime}ms`);
|
||
response.addResult('');
|
||
response.addResult('⚙️ **Configuration:**');
|
||
response.addResult(`• Attached to Page: ${status.isAttached ? 'Yes' : 'No'}`);
|
||
response.addResult(`• Current Page: ${status.pageUrl || 'None'}`);
|
||
response.addResult(`• Capture Bodies: ${status.options.captureBody ? 'Yes' : 'No'}`);
|
||
response.addResult(`• Max Body Size: ${status.options.maxBodySize ? (status.options.maxBodySize / 1024 / 1024).toFixed(1) + 'MB' : 'Unlimited'}`);
|
||
response.addResult(`• Auto Save: ${status.options.autoSave ? 'Yes' : 'No'}`);
|
||
response.addResult(`• Output Path: ${status.options.outputPath || 'Default'}`);
|
||
|
||
if (stats.totalRequests > 0) {
|
||
response.addResult('');
|
||
response.addResult('📈 **Recent Activity:**');
|
||
const recentRequests = interceptor.getData().slice(-3);
|
||
recentRequests.forEach((req, index) => {
|
||
const duration = req.duration ? ` (${req.duration}ms)` : '';
|
||
const status = req.failed ? 'FAILED' : req.response?.status || 'pending';
|
||
response.addResult(` ${index + 1}. ${req.method} ${status} - ${new URL(req.url).pathname}${duration}`);
|
||
});
|
||
}
|
||
|
||
} catch (error: any) {
|
||
throw new Error(`Failed to get monitoring status: ${error.message}`);
|
||
}
|
||
},
|
||
});
|
||
|
||
export default [
|
||
startRequestMonitoring,
|
||
getRequests,
|
||
exportRequests,
|
||
clearRequests,
|
||
getMonitoringStatus,
|
||
];
|