learning_ai_common_plat/packages/diagnostics-client/src/network.ts
saravanakumardb1 18dd263797 feat(sdk): Push deep link routing for all platforms
- TypeScript: DeepLinkRouter with URL parsing and handler registration
- Swift: BLDeepLinkRouter with iOS URL handling and Logger integration
- Kotlin: DeepLinkRouter with Android Uri parsing and handler mapping
- Common screen constants: broadcasts, surveys, settings, profile, etc.
2026-03-03 08:33:56 -08:00

215 lines
5.9 KiB
TypeScript

/**
* Network interceptor — capture HTTP requests/responses
*
* @module network
*/
import type { NetworkRequest } from './types.js';
// DOM type declarations for ESLint
type RequestInfo = string | Request | URL;
type HeadersInit = Headers | Record<string, string> | string[][];
export interface NetworkInterceptorOptions {
/** URL patterns to include (default: all) */
includePatterns?: RegExp[];
/** URL patterns to exclude */
excludePatterns?: RegExp[];
/** Max request body size to capture (default: 100KB) */
maxBodySize?: number;
/** Whether to capture request headers (default: true) */
captureRequestHeaders?: boolean;
/** Whether to capture response headers (default: true) */
captureResponseHeaders?: boolean;
/** Sanitize header values matching these patterns */
sensitiveHeaderPatterns?: RegExp[];
}
/**
* Interceptor for capturing network requests
*/
export class NetworkInterceptor {
private options: Required<NetworkInterceptorOptions>;
private originalFetch: typeof fetch;
private isActive = false;
private pendingRequests = new Map<string, NetworkRequest>();
private onRequest: (request: NetworkRequest) => void;
constructor(
onRequest: (request: NetworkRequest) => void,
options: NetworkInterceptorOptions = {}
) {
this.onRequest = onRequest;
this.options = {
includePatterns: options.includePatterns ?? [],
excludePatterns: options.excludePatterns ?? [],
maxBodySize: options.maxBodySize ?? 100 * 1024,
captureRequestHeaders: options.captureRequestHeaders ?? true,
captureResponseHeaders: options.captureResponseHeaders ?? true,
sensitiveHeaderPatterns: options.sensitiveHeaderPatterns ?? [
/authorization/i,
/cookie/i,
/token/i,
/api-key/i,
],
};
this.originalFetch = globalThis.fetch.bind(globalThis);
}
/**
* Start intercepting fetch calls
*/
start(): void {
if (this.isActive) return;
this.isActive = true;
globalThis.fetch = this.interceptedFetch.bind(this);
}
/**
* Stop intercepting fetch calls
*/
stop(): void {
if (!this.isActive) return;
this.isActive = false;
globalThis.fetch = this.originalFetch;
}
/**
* Check if URL should be captured
*/
private shouldCapture(url: string): boolean {
// Check excludes first
for (const pattern of this.options.excludePatterns) {
if (pattern.test(url)) return false;
}
// If includes specified, must match one
if (this.options.includePatterns.length > 0) {
for (const pattern of this.options.includePatterns) {
if (pattern.test(url)) return true;
}
return false;
}
return true;
}
/**
* Sanitize headers
*/
private sanitizeHeaders(
headers: HeadersInit | undefined
): Record<string, string> {
const sanitized: Record<string, string> = {};
const headerEntries = headers instanceof Headers
? Array.from(headers.entries())
: typeof headers === 'object' && headers !== null
? Object.entries(headers)
: [];
for (const [key, value] of headerEntries) {
const isSensitive = this.options.sensitiveHeaderPatterns.some(p =>
p.test(key)
);
sanitized[key] = isSensitive ? '[REDACTED]' : value;
}
return sanitized;
}
/**
* Generate request ID
*/
private generateId(): string {
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
}
/**
* Intercepted fetch implementation
*/
private async interceptedFetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const url = input.toString();
const shouldCapture = this.shouldCapture(url);
const requestId = this.generateId();
const startTime = new Date().toISOString();
// Create request record if capturing
if (shouldCapture) {
const request: NetworkRequest = {
id: requestId,
url: url.slice(0, 2048), // Limit URL length
method: (init?.method ?? 'GET').toUpperCase(),
requestHeaders: this.options.captureRequestHeaders
? this.sanitizeHeaders(init?.headers)
: {},
startTime,
};
// Capture request body if present and not too large
if (init?.body && typeof init.body === 'string') {
if (init.body.length <= this.options.maxBodySize) {
request.requestBody = init.body.slice(0, this.options.maxBodySize);
}
}
this.pendingRequests.set(requestId, request);
}
try {
const response = await this.originalFetch(input, init);
// Update with response info if capturing
if (shouldCapture) {
const request = this.pendingRequests.get(requestId);
if (request) {
request.status = response.status;
request.endTime = new Date().toISOString();
request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();
if (this.options.captureResponseHeaders) {
request.responseHeaders = this.sanitizeHeaders(
Object.fromEntries(response.headers.entries())
);
}
// Don't capture response body (too large/complex)
// Just record that we received a response
this.pendingRequests.delete(requestId);
this.onRequest(request);
}
}
return response;
} catch (error) {
// Record error if capturing
if (shouldCapture) {
const request = this.pendingRequests.get(requestId);
if (request) {
request.endTime = new Date().toISOString();
request.durationMs = new Date(request.endTime).getTime() - new Date(startTime).getTime();
request.error = error instanceof Error ? error.message : String(error);
this.pendingRequests.delete(requestId);
this.onRequest(request);
}
}
throw error;
}
}
/**
* Check if interceptor is active
*/
isRunning(): boolean {
return this.isActive;
}
}