diff --git a/clients/ts/FunctionalTests/ts/TestLogger.ts b/clients/ts/FunctionalTests/ts/TestLogger.ts index e3fa685dde..1d9e324f60 100644 --- a/clients/ts/FunctionalTests/ts/TestLogger.ts +++ b/clients/ts/FunctionalTests/ts/TestLogger.ts @@ -1,12 +1,17 @@ -import { ILogger, LogLevel } from "@aspnet/signalr"; +import { ConsoleLogger, ILogger, LogLevel } from "@aspnet/signalr"; export class TestLogger implements ILogger { public static instance: TestLogger = new TestLogger(); + private static consoleLogger: ConsoleLogger = new ConsoleLogger(LogLevel.Trace); public messages: Array<[LogLevel, string]> = []; public log(logLevel: LogLevel, message: string): void { + // Capture log message so it can be reported later this.messages.push([logLevel, message]); + + // Also write to browser console + TestLogger.consoleLogger.log(logLevel, message); } public static getMessagesAndReset(): Array<[LogLevel, string]> { diff --git a/clients/ts/signalr/src/HttpClient.ts b/clients/ts/signalr/src/HttpClient.ts index d3996e7e6b..2be9686264 100644 --- a/clients/ts/signalr/src/HttpClient.ts +++ b/clients/ts/signalr/src/HttpClient.ts @@ -3,6 +3,7 @@ import { AbortSignal } from "./AbortController"; import { HttpError, TimeoutError } from "./Errors"; +import { ILogger, LogLevel } from "./ILogger"; export interface HttpRequest { method?: string; @@ -49,6 +50,13 @@ export abstract class HttpClient { } export class DefaultHttpClient extends HttpClient { + private readonly logger: ILogger; + + constructor(logger: ILogger) { + super(); + this.logger = logger; + } + public send(request: HttpRequest): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -89,10 +97,12 @@ export class DefaultHttpClient extends HttpClient { }; xhr.onerror = () => { + this.logger.log(LogLevel.Warning, `Error from HTTP request. ${xhr.status}: ${xhr.statusText}`); reject(new HttpError(xhr.statusText, xhr.status)); }; xhr.ontimeout = () => { + this.logger.log(LogLevel.Warning, `Timeout from HTTP request.`); reject(new TimeoutError()); }; diff --git a/clients/ts/signalr/src/HttpConnection.ts b/clients/ts/signalr/src/HttpConnection.ts index febd61c21c..4efa4867c5 100644 --- a/clients/ts/signalr/src/HttpConnection.ts +++ b/clients/ts/signalr/src/HttpConnection.ts @@ -54,7 +54,7 @@ export class HttpConnection implements IConnection { options = options || {}; options.accessTokenFactory = options.accessTokenFactory || (() => null); - this.httpClient = options.httpClient || new DefaultHttpClient(); + this.httpClient = options.httpClient || new DefaultHttpClient(this.logger); this.connectionState = ConnectionState.Disconnected; this.options = options; } @@ -63,6 +63,8 @@ export class HttpConnection implements IConnection { Arg.isRequired(transferFormat, "transferFormat"); Arg.isIn(transferFormat, TransferFormat, "transferFormat"); + this.logger.log(LogLevel.Trace, `Starting connection with transfer format '${TransferFormat[transferFormat]}'.`); + if (this.connectionState !== ConnectionState.Disconnected) { return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state.")); } @@ -114,11 +116,18 @@ export class HttpConnection implements IConnection { } private async getNegotiationResponse(headers: any): Promise { - const response = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), { - content: "", - headers, - }); - return JSON.parse(response.content as string); + const negotiateUrl = this.resolveNegotiateUrl(this.baseUrl); + this.logger.log(LogLevel.Trace, `Sending negotiation request: ${negotiateUrl}`); + try { + const response = await this.httpClient.post(negotiateUrl, { + content: "", + headers, + }); + return JSON.parse(response.content as string); + } catch (e) { + this.logger.log(LogLevel.Error, "Failed to complete negotiation with the server: " + e); + throw e; + } } private updateConnectionId(negotiateResponse: INegotiateResponse) { @@ -154,7 +163,7 @@ export class HttpConnection implements IConnection { this.changeState(ConnectionState.Connecting, ConnectionState.Connected); return; } catch (ex) { - this.logger.log(LogLevel.Error, `Failed to start the transport' ${TransportType[transport]}:' transport'${ex}'`); + this.logger.log(LogLevel.Error, `Failed to start the transport '${TransportType[transport]}': ${ex}`); this.connectionState = ConnectionState.Disconnected; negotiateResponse.connectionId = null; } diff --git a/clients/ts/signalr/src/HubConnection.ts b/clients/ts/signalr/src/HubConnection.ts index 8fd97c3d13..2757d77603 100644 --- a/clients/ts/signalr/src/HubConnection.ts +++ b/clients/ts/signalr/src/HubConnection.ts @@ -204,10 +204,13 @@ export class HubConnection { } public async start(): Promise { + this.logger.log(LogLevel.Trace, "Starting HubConnection."); + this.receivedHandshakeResponse = false; await this.connection.start(this.protocol.transferFormat); + this.logger.log(LogLevel.Trace, "Sending handshake request."); // Handshake request is always JSON await this.connection.send( TextMessageFormat.write( @@ -221,6 +224,8 @@ export class HubConnection { } public stop(): Promise { + this.logger.log(LogLevel.Trace, "Stopping HubConnection."); + this.cleanupTimeout(); return this.connection.stop(); } diff --git a/clients/ts/signalr/src/Transports.ts b/clients/ts/signalr/src/Transports.ts index 8827d668b9..7e142019bd 100644 --- a/clients/ts/signalr/src/Transports.ts +++ b/clients/ts/signalr/src/Transports.ts @@ -48,6 +48,8 @@ export class WebSocketTransport implements ITransport { throw new Error("'WebSocket' is not supported in your environment."); } + this.logger.log(LogLevel.Trace, "(WebSockets transport) Connecting"); + return new Promise((resolve, reject) => { url = url.replace(/^http/, "ws"); const token = this.accessTokenFactory(); @@ -71,7 +73,7 @@ export class WebSocketTransport implements ITransport { }; webSocket.onmessage = (message: MessageEvent) => { - this.logger.log(LogLevel.Trace, `(WebSockets transport) data received. Length ${getDataLength(message.data)}.`); + this.logger.log(LogLevel.Trace, `(WebSockets transport) data received. ${getDataDetail(message.data)}.`); if (this.onreceive) { this.onreceive(message.data); } @@ -92,6 +94,7 @@ export class WebSocketTransport implements ITransport { public send(data: any): Promise { if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) { + this.logger.log(LogLevel.Trace, `(WebSockets transport) sending data. ${getDataDetail(data)}.`); this.webSocket.send(data); return Promise.resolve(); } @@ -134,6 +137,8 @@ export class ServerSentEventsTransport implements ITransport { throw new Error("'EventSource' is not supported in your environment."); } + this.logger.log(LogLevel.Trace, "(SSE transport) Connecting"); + this.url = url; return new Promise((resolve, reject) => { if (transferFormat !== TransferFormat.Text) { @@ -151,7 +156,7 @@ export class ServerSentEventsTransport implements ITransport { eventSource.onmessage = (e: MessageEvent) => { if (this.onreceive) { try { - this.logger.log(LogLevel.Trace, `(SSE transport) data received. Length ${getDataLength(e.data)}.`); + this.logger.log(LogLevel.Trace, `(SSE transport) data received. ${getDataDetail(e.data)}.`); this.onreceive(e.data); } catch (error) { if (this.onclose) { @@ -184,7 +189,7 @@ export class ServerSentEventsTransport implements ITransport { } public async send(data: any): Promise { - return send(this.httpClient, this.url, this.accessTokenFactory, data); + return send(this.logger, "SSE", this.httpClient, this.url, this.accessTokenFactory, data); } public stop(): Promise { @@ -223,6 +228,8 @@ export class LongPollingTransport implements ITransport { this.url = url; + this.logger.log(LogLevel.Trace, "(LongPolling transport) Connecting"); + // Set a flag indicating we have inherent keep-alive in this transport. connection.features.inherentKeepAlive = true; @@ -276,7 +283,7 @@ export class LongPollingTransport implements ITransport { } else { // Process the response if (response.content) { - this.logger.log(LogLevel.Trace, `(LongPolling transport) data received. Length ${getDataLength(response.content)}.`); + this.logger.log(LogLevel.Trace, `(LongPolling transport) data received. ${getDataDetail(response.content)}.`); if (this.onreceive) { this.onreceive(response.content); } @@ -301,7 +308,7 @@ export class LongPollingTransport implements ITransport { } public async send(data: any): Promise { - return send(this.httpClient, this.url, this.accessTokenFactory, data); + return send(this.logger, "LongPolling", this.httpClient, this.url, this.accessTokenFactory, data); } public stop(): Promise { @@ -313,17 +320,17 @@ export class LongPollingTransport implements ITransport { public onclose: TransportClosed; } -function getDataLength(data: any): number { - let length: number = null; +function getDataDetail(data: any): string { + let length: string = null; if (data instanceof ArrayBuffer) { - length = data.byteLength; - } else if (data instanceof String) { - length = data.length; + length = `Binary data of length ${data.byteLength}`; + } else if (typeof data === "string") { + length = `String data of length ${data.length}`; } return length; } -async function send(httpClient: HttpClient, url: string, accessTokenFactory: () => string, content: string | ArrayBuffer): Promise { +async function send(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: () => string, content: string | ArrayBuffer): Promise { let headers; const token = accessTokenFactory(); if (token) { @@ -332,8 +339,12 @@ async function send(httpClient: HttpClient, url: string, accessTokenFactory: () }; } - await httpClient.post(url, { + logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content)}.`); + + const response = await httpClient.post(url, { content, headers, }); + + logger.log(LogLevel.Trace, `(${transportName} transport) request complete. Response status: ${response.statusCode}.`); }