Repository: microsoft/ApplicationInsights-node.js
Pull request #538 (2019-07-19 18:19:41)

Add ConnectionStringParser
Adds `ConnectionStringParser`, which parses both in-code and environment variable connection strings and combines them based on code > environment variable. Instrumentation key priority is `.setup(CS)` > `.setup(ikey)` > CS env var > ikey env var.
@Mikhail-msft

TODOS
- [x] Add CS parsing in `.setup(...)`



TimothyMothra: Hey @markwolff,
It looks like you covered all the correct use cases in your tests. :)
I don't see many tests for invalid cases; Incorrect delimiters, duplicate keys, spaces in key.
You may want to consider adding some tests to verify that you can detect invalid connection strings without crashing and can provide meaningful error messages so customers can self diagnose.

Otherwise, this LGTM.
markwolff: > Hey @markwolff,
> It looks like you covered all the correct use cases in your tests. :)
> I don't see many tests for invalid cases; Incorrect delimiters, duplicate keys, spaces in key.
> You may want to consider adding some tests to verify that you can detect invalid connection strings without crashing and can provide meaningful error messages so customers can self diagnose.
> 
> Otherwise, this LGTM.

Synced with Mothra offline. Will merge PR this in now and bring validation testing&logging to .NET parity in a separate PR.

Patch:
diff --git a/Declarations/Constants.ts b/Declarations/Constants.ts
index 024deb2..d0151a2 100644
--- a/Declarations/Constants.ts
+++ b/Declarations/Constants.ts
@@ -1,5 +1,9 @@
 import Contracts = require("./Contracts")
 
+export const DEFAULT_BREEZE_ENDPOINT = "https://dc.services.visualstudio.com";
+export const DEFAULT_LIVEMETRICS_ENDPOINT = "https://rt.services.visualstudio.com";
+export const DEFAULT_LIVEMETRICS_HOST = "rt.services.visualstudio.com";
+
 export enum QuickPulseCounter {
     // Memory
     COMMITTED_BYTES= "\\Memory\\Committed Bytes",
diff --git a/Declarations/Contracts/Constants.ts b/Declarations/Contracts/Constants.ts
index 6e1c5d7..5cad943 100644
--- a/Declarations/Contracts/Constants.ts
+++ b/Declarations/Contracts/Constants.ts
@@ -18,4 +18,23 @@ export function domainSupportsProperties(domain: Domain): domain is ISupportProp
         domain instanceof PageViewData ||
         domain instanceof RemoteDependencyData ||
         domain instanceof RequestData;
-}
\ No newline at end of file
+}
+
+/**
+ * Subset of Connection String fields which this SDK can parse. Lower-typecased to
+ * allow for case-insensitivity across field names
+ * @type ConnectionStringKey
+ */
+export interface ConnectionString {
+    authorization?: string;
+    instrumentationkey?: string;
+    ingestionendpoint?: string;
+    liveendpoint?: string;
+    location?: string;
+    endpointsuffix?: string;
+
+    // Note: this is a node types backcompat equivalent to
+    // type ConnectionString = { [key in ConnectionStringKey]?: string }
+}
+
+export type ConnectionStringKey = "authorization" | "instrumentationkey" | "ingestionendpoint" | "liveendpoint" | "location" | "endpointsuffix";
diff --git a/Library/Config.ts b/Library/Config.ts
index 7824c56..2869a10 100644
--- a/Library/Config.ts
+++ b/Library/Config.ts
@@ -1,9 +1,11 @@
 import CorrelationIdManager = require('./CorrelationIdManager');
+import ConnectionStringParser = require('./ConnectionStringParser');
+import Constants = require('../Declarations/Constants');
 import http = require('http');
 import https = require('https');
+import url = require('url');
 
 class Config {
-
     // Azure adds this prefix to all environment variables
     public static ENV_azurePrefix = "APPSETTING_";
 
@@ -13,6 +15,9 @@ class Config {
     public static ENV_profileQueryEndpoint = "APPINSIGHTS_PROFILE_QUERY_ENDPOINT";
     public static ENV_quickPulseHost = "APPINSIGHTS_QUICKPULSE_HOST";
 
+    // Azure Connection String
+    public static ENV_connectionString = "APPLICATIONINSIGHTS_CONNECTION_STRING";
+
     // Native Metrics Opt Outs
     public static ENV_nativeMetricsDisablers = "APPLICATION_INSIGHTS_DISABLE_EXTENDED_METRIC";
     public static ENV_nativeMetricsDisableAll = "APPLICATION_INSIGHTS_DISABLE_ALL_EXTENDED_METRICS"
@@ -47,15 +52,24 @@ class Config {
     /** An https.Agent to use for SDK HTTPS traffic (Optional, Default undefined) */
     public httpsAgent: https.Agent;
 
-    private endpointBase: string = "https://dc.services.visualstudio.com";
+    private endpointBase: string = Constants.DEFAULT_BREEZE_ENDPOINT;
     private setCorrelationId: (v: string) => void;
     private _profileQueryEndpoint: string;
     /** Host name for quickpulse service */
     private _quickPulseHost: string;
 
-    constructor(instrumentationKey?: string) {
-        this.instrumentationKey = instrumentationKey || Config._getInstrumentationKey();
-        this.endpointUrl = `${this.endpointBase}/v2/track`;
+
+    constructor(setupString?: string) {
+        const connectionStringEnv: string | undefined = process.env[Config.ENV_connectionString];
+        const csCode = ConnectionStringParser.parse(setupString);
+        const csEnv = ConnectionStringParser.parse(connectionStringEnv);
+        const iKeyCode = !csCode.instrumentationkey && Object.keys(csCode).length > 0
+            ? null // CS was valid but instrumentation key was not provided, null and grab from env var
+            : setupString; // CS was invalid, so it must be an ikey
+
+        this.instrumentationKey = csCode.instrumentationkey || iKeyCode /* === instrumentationKey */ || csEnv.instrumentationkey || Config._getInstrumentationKey();
+
+        this.endpointUrl = `${csCode.ingestionendpoint || csEnv.ingestionendpoint || this.endpointBase}/v2/track`;
         this.maxBatchSize = 250;
         this.maxBatchIntervalMs = 15000;
         this.disableAppInsights = false;
@@ -69,12 +83,16 @@ class Config {
 
         this.setCorrelationId = (correlationId) => this.correlationId = correlationId;
 
-        this.profileQueryEndpoint = process.env[Config.ENV_profileQueryEndpoint] || this.endpointBase;
+        this.profileQueryEndpoint = csCode.ingestionendpoint || csEnv.ingestionendpoint || process.env[Config.ENV_profileQueryEndpoint] || this.endpointBase;
         this.proxyHttpUrl = process.env[Config.ENV_http_proxy] || undefined;
         this.proxyHttpsUrl = process.env[Config.ENV_https_proxy] || undefined;
         this.httpAgent = undefined;
         this.httpsAgent = undefined;
-        this._quickPulseHost = process.env[Config.ENV_quickPulseHost] || "rt.services.visualstudio.com";
+        this._quickPulseHost = csCode.liveendpoint || csEnv.liveendpoint || process.env[Config.ENV_quickPulseHost] || Constants.DEFAULT_LIVEMETRICS_HOST;
+        // Parse quickPulseHost if it startswith http(s)://
+        if (this._quickPulseHost.match(/^https?:\/\//)) {
+            this._quickPulseHost = url.parse(this._quickPulseHost).host;
+        }
     }
 
     public set profileQueryEndpoint(endpoint: string) {
diff --git a/Library/ConnectionStringParser.ts b/Library/ConnectionStringParser.ts
new file mode 100644
index 0000000..90f4003
--- /dev/null
+++ b/Library/ConnectionStringParser.ts
@@ -0,0 +1,45 @@
+import { ConnectionString, ConnectionStringKey } from "../Declarations/Contracts";
+import Constants = require("../Declarations/Constants");
+
+class ConnectionStringParser {
+    private static _FIELDS_SEPARATOR = ";";
+    private static _FIELD_KEY_VALUE_SEPARATOR = "=";
+
+    public static parse(connectionString?: string): ConnectionString {
+        if (!connectionString) {
+            return {};
+        }
+
+        const kvPairs = connectionString.split(ConnectionStringParser._FIELDS_SEPARATOR);
+
+        const result: ConnectionString = kvPairs.reduce((fields: ConnectionString, kv: string) => {
+            const kvParts = kv.split(ConnectionStringParser._FIELD_KEY_VALUE_SEPARATOR);
+
+            if (kvParts.length === 2) { // only save fields with valid formats
+                const key = kvParts[0].toLowerCase() as ConnectionStringKey;
+                const value = kvParts[1];
+                fields[key] = value as string;
+            }
+            return fields;
+        }, {});
+
+        if (Object.keys(result).length > 0) {
+            // this is a valid connection string, so parse the results
+
+            if (result.endpointsuffix) {
+                // use endpoint suffix where overrides are not provided
+                const locationPrefix = result.location ? result.location + "." : "";
+                result.ingestionendpoint = result.ingestionendpoint || ("https://" + locationPrefix + "dc." + result.endpointsuffix);
+                result.liveendpoint = result.liveendpoint || ("https://" + locationPrefix + "live." + result.endpointsuffix);
+            }
+
+            // apply the default endpoints
+            result.ingestionendpoint = result.ingestionendpoint || Constants.DEFAULT_BREEZE_ENDPOINT;
+            result.liveendpoint = result.liveendpoint || Constants.DEFAULT_LIVEMETRICS_ENDPOINT;
+        }
+
+        return result;
+    }
+}
+
+export = ConnectionStringParser;
diff --git a/Library/TelemetryClient.ts b/Library/TelemetryClient.ts
index 4de2579..68faa63 100644
--- a/Library/TelemetryClient.ts
+++ b/Library/TelemetryClient.ts
@@ -29,10 +29,10 @@ class TelemetryClient {
 
     /**
      * Constructs a new client of the client
-     * @param iKey the instrumentation key to use (read from environment variable if not specified)
+     * @param setupString the Connection String or Instrumentation Key to use (read from environment variable if not specified)
      */
-    constructor(iKey?: string) {
-        var config = new Config(iKey);
+    constructor(setupString?: string) {
+        var config = new Config(setupString);
         this.config = config;
         this.context = new Context();
         this.commonProperties = {};
diff --git a/applicationinsights.ts b/applicationinsights.ts
index 5da315b..efcf55c 100644
--- a/applicationinsights.ts
+++ b/applicationinsights.ts
@@ -69,15 +69,15 @@ let _performanceLiveMetrics: AutoCollectPerformance;
  * Initializes the default client. Should be called after setting
  * configuration options.
  *
- * @param instrumentationKey the instrumentation key to use. Optional, if
+ * @param setupString the Connection String or Instrumentation Key to use. Optional, if
  * this is not specified, the value will be read from the environment
- * variable APPINSIGHTS_INSTRUMENTATIONKEY.
+ * variable APPLICATIONINSIGHTS_CONNECTION_STRING or APPINSIGHTS_INSTRUMENTATIONKEY.
  * @returns {Configuration} the configuration class to initialize
  * and start the SDK.
  */
-export function setup(instrumentationKey?: string) {
+export function setup(setupString?: string) {
     if(!defaultClient) {
-        defaultClient = new TelemetryClient(instrumentationKey);
+        defaultClient = new TelemetryClient(setupString);
         _console = new AutoCollectConsole(defaultClient);
         _exceptions = new AutoCollectExceptions(defaultClient);
         _performance = new AutoCollectPerformance(defaultClient);


Test patch:
diff --git a/Tests/Library/Config.tests.ts b/Tests/Library/Config.tests.ts
index 022284f..9d3ac94 100644
--- a/Tests/Library/Config.tests.ts
+++ b/Tests/Library/Config.tests.ts
@@ -4,86 +4,137 @@ var http = require("http");
 var https = require("https");
 
 import Config = require("../../Library/Config");
+import Constants = require("../../Declarations/Constants");
 
 describe("Library/Config", () => {
 
     var iKey = "iKey";
     var appVer = "appVer";
 
-    describe("#constructor(iKey)", () => {
-        beforeEach(()=> {
-            sinon.stub(http, 'request');
-            sinon.stub(https, 'request');
-        });
-        afterEach(() => {
-            http.request.restore();
-            https.request.restore();
-        })
-        it("should throw if no iKey is available", () => {
-            var env = {};
-            var originalEnv = process.env;
-            process.env = env;
-            assert.throws(() => new Config());
-            process.env = originalEnv;
-        });
+    describe("#constructor", () => {
+        describe("connection string && API && environment variable prioritization", () => {
+            it ("connection string set via in code setup", () => {
+                var env = { [Config.ENV_connectionString]: "InStruMenTatioNKey=cs.env", [Config.ENV_iKey]: "ikey.env"};
+                var originalEnv = process.env;
+                process.env = env;
+                const config = new Config("InStruMenTatioNKey=cs.code");
+                assert.deepEqual(config.instrumentationKey, "cs.code");
+                process.env = originalEnv;
+            });
 
-        it("should read iKey from environment", () => {
-            var env = <{[id: string]: string}>{};
-            env[Config.ENV_iKey] = iKey;
-            var originalEnv = process.env;
-            process.env = env;
-            var config = new Config();
-            assert.equal(config.instrumentationKey, iKey);
-            process.env = originalEnv;
-        });
+            it("instrumentation key set via in code setup", () => {
+                var env = { [Config.ENV_connectionString]: "InStruMenTatioNKey=CS.env", [Config.ENV_iKey]: "ikey.env"};
+                var originalEnv = process.env;
+                process.env = env;
+                const config = new Config("ikey.code");
+                assert.deepEqual(config.instrumentationKey, "ikey.code");
+                process.env = originalEnv;
+            });
 
-        it("should read iKey from azure environment", () => {
-            var env = <{[id: string]: string}>{};
-            env[Config.ENV_azurePrefix + Config.ENV_iKey] = iKey;
-            var originalEnv = process.env;
-            process.env = env;
-            var config = new Config();
-            assert.equal(config.instrumentationKey, iKey);
-            process.env = originalEnv;
-        });
+            it("connection string set via environment variable", () => {
+                var env = { [Config.ENV_connectionString]: "InStruMenTatioNKey=cs.env", [Config.ENV_iKey]: "ikey.env"};
+                var originalEnv = process.env;
+                process.env = env;
+                const config = new Config();
+                assert.deepEqual(config.instrumentationKey, "cs.env");
+                process.env = originalEnv;
+            });
 
-        it("should initialize valid values", () => {
-            var config = new Config("iKey");
-            assert(typeof config.instrumentationKey === "string");
-            assert(typeof config.endpointUrl === "string");
-            assert(typeof config.maxBatchSize === "number");
-            assert(typeof config.maxBatchIntervalMs === "number");
-            assert(typeof config.disableAppInsights === "boolean");
-            assert(typeof config.samplingPercentage === "number");
-            assert(typeof config.correlationIdRetryIntervalMs === "number");
-            assert(typeof config.correlationHeaderExcludedDomains === "object");
-            assert(typeof config.quickPulseHost === "string");
-        });
+            it("instrumentation key set via environment variable", () => {
+                var env = { [Config.ENV_iKey]: "ikey.env"};
+                var originalEnv = process.env;
+                process.env = env;
+                const config = new Config();
+                assert.deepEqual(config.instrumentationKey, "ikey.env");
+                process.env = originalEnv;
+            });
 
-        it("should initialize values that we claim in README", () => {
-            var config = new Config("iKey");
-            assert(config.maxBatchSize === 250);
-            assert(config.maxBatchIntervalMs === 15000);
-            assert(config.disableAppInsights === false);
-            assert(config.samplingPercentage === 100);
-            assert(config.correlationIdRetryIntervalMs === 30000);
-            assert(config.proxyHttpUrl === undefined);
-            assert(config.proxyHttpsUrl === undefined);
-
-            assert(config.quickPulseHost === "rt.services.visualstudio.com");
-        });
+            it("should parse the host of livemetrics host, if provided", () => {
+                const config = new Config("InStruMenTatioNKey=ikey;LiveEndpoint=https://live.applicationinsights.io/foo/bar");
+                assert.deepEqual(config.quickPulseHost, "live.applicationinsights.io");
+            });
 
-        it("should initialize values that we claim in README (2)", () => {
-            process.env.http_proxy = "test";
-            process.env.https_proxy = "test2";
-            var config = new Config("iKey");
-            assert(config.proxyHttpUrl === "test");
-            assert(config.proxyHttpsUrl === "test2");
+            it("should parse the host of livemetrics host from location+suffix, if provided", () => {
+                const config = new Config("InStruMenTatioNKey=ikey;Location=wus2;EndpointSuffix=example.com");
+                assert.deepEqual(config.quickPulseHost, "wus2.live.example.com");
+            });
         });
 
-        it("should add azure domain to excluded list", () => {
-            var config = new Config("iKey");
-            assert.equal(config.correlationHeaderExcludedDomains[0].toString(), "*.core.windows.net");
+        describe("constructor(ikey)", () => {
+            beforeEach(()=> {
+                sinon.stub(http, 'request');
+                sinon.stub(https, 'request');
+            });
+            afterEach(() => {
+                http.request.restore();
+                https.request.restore();
+            });
+            it("should throw if no iKey is available", () => {
+                var env = {};
+                var originalEnv = process.env;
+                process.env = env;
+                assert.throws(() => new Config());
+                process.env = originalEnv;
+            });
+
+            it("should read iKey from environment", () => {
+                var env = <{[id: string]: string}>{};
+                env[Config.ENV_iKey] = iKey;
+                var originalEnv = process.env;
+                process.env = env;
+                var config = new Config();
+                assert.equal(config.instrumentationKey, iKey);
+                process.env = originalEnv;
+            });
+
+            it("should read iKey from azure environment", () => {
+                var env = <{[id: string]: string}>{};
+                env[Config.ENV_azurePrefix + Config.ENV_iKey] = iKey;
+                var originalEnv = process.env;
+                process.env = env;
+                var config = new Config();
+                assert.equal(config.instrumentationKey, iKey);
+                process.env = originalEnv;
+            });
+
+            it("should initialize valid values", () => {
+                var config = new Config("iKey");
+                assert(typeof config.instrumentationKey === "string");
+                assert(typeof config.endpointUrl === "string");
+                assert(typeof config.maxBatchSize === "number");
+                assert(typeof config.maxBatchIntervalMs === "number");
+                assert(typeof config.disableAppInsights === "boolean");
+                assert(typeof config.samplingPercentage === "number");
+                assert(typeof config.correlationIdRetryIntervalMs === "number");
+                assert(typeof config.correlationHeaderExcludedDomains === "object");
+                assert(typeof config.quickPulseHost === "string");
+            });
+
+            it("should initialize values that we claim in README", () => {
+                var config = new Config("iKey");
+                assert(config.maxBatchSize === 250);
+                assert(config.maxBatchIntervalMs === 15000);
+                assert(config.disableAppInsights === false);
+                assert(config.samplingPercentage === 100);
+                assert(config.correlationIdRetryIntervalMs === 30000);
+                assert(config.proxyHttpUrl === undefined);
+                assert(config.proxyHttpsUrl === undefined);
+
+                assert(config.quickPulseHost === Constants.DEFAULT_LIVEMETRICS_HOST);
+            });
+
+            it("should initialize values that we claim in README (2)", () => {
+                process.env.http_proxy = "test";
+                process.env.https_proxy = "test2";
+                var config = new Config("iKey");
+                assert(config.proxyHttpUrl === "test");
+                assert(config.proxyHttpsUrl === "test2");
+            });
+
+            it("should add azure domain to excluded list", () => {
+                var config = new Config("iKey");
+                assert.equal(config.correlationHeaderExcludedDomains[0].toString(), "*.core.windows.net");
+            });
         });
     });
 });
diff --git a/Tests/Library/ConnectionStringParser.tests.ts b/Tests/Library/ConnectionStringParser.tests.ts
new file mode 100644
index 0000000..6d4dd4c
--- /dev/null
+++ b/Tests/Library/ConnectionStringParser.tests.ts
@@ -0,0 +1,104 @@
+import assert = require("assert");
+
+import sinon = require("sinon");
+import Constants = require("../../Declarations/Constants");
+import ConnectionStringParser = require("../../Library/ConnectionStringParser");
+
+describe("ConnectionStringParser", () => {
+    describe("#parse()", () => {
+        it("should parse all valid fields", () => {
+            const authorization = "ikey"
+            const instrumentationKey = "instr_key";
+            const ingestionEndpoint = "ingest";
+            const liveEndpoint = "live";
+            const connectionString = `Authorization=${authorization};InstrumentationKey=${instrumentationKey};IngestionEndpoint=${ingestionEndpoint};LiveEndpoint=${liveEndpoint}`;
+
+            const result = ConnectionStringParser.parse(connectionString);
+
+            assert.deepEqual(result.authorization, authorization);
+            assert.deepEqual(result.instrumentationkey, instrumentationKey);
+            assert.deepEqual(result.ingestionendpoint, ingestionEndpoint);
+            assert.deepEqual(result.liveendpoint, liveEndpoint);
+        });
+
+        it("should ignore invalid fields", () => {
+            const authorization = "ikey"
+            const instrumentationKey = "ikey";
+            const ingestionEndpoint = "ingest";
+            const liveEndpoint = "live";
+            const connectionString = `Autho.rization=${authorization};Instrume.ntationKey=${instrumentationKey};Ingestion.Endpoint=${ingestionEndpoint};LiveEnd.point=${liveEndpoint}`;
+
+            const result = ConnectionStringParser.parse(connectionString);
+
+            assert.deepEqual(result.authorization, undefined);
+            assert.deepEqual(result.instrumentationkey, undefined);
+            assert.deepEqual(result.ingestionendpoint, Constants.DEFAULT_BREEZE_ENDPOINT);
+            assert.deepEqual(result.liveendpoint, Constants.DEFAULT_LIVEMETRICS_ENDPOINT);
+        });
+
+        const runTest = (options: {
+            connectionString: string,
+            expectedAuthorization?: string,
+            expectedInstrumentationKey?: string,
+            expectedBreezeEndpoint: string,
+            expectedLiveMetricsEndpoint: string,
+        }) => {
+            const result = ConnectionStringParser.parse(options.connectionString);
+
+            if (options.expectedAuthorization) assert.deepEqual(result.authorization, options.expectedAuthorization);
+            if (options.expectedInstrumentationKey) assert.deepEqual(result.instrumentationkey, options.expectedInstrumentationKey);
+            assert.deepEqual(result.ingestionendpoint, options.expectedBreezeEndpoint);
+            assert.deepEqual(result.liveendpoint, options.expectedLiveMetricsEndpoint);
+        }
+
+        it("should use correct default endpoints", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000",
+                expectedAuthorization: undefined,
+                expectedInstrumentationKey: "00000000-0000-0000-0000-000000000000",
+                expectedBreezeEndpoint: Constants.DEFAULT_BREEZE_ENDPOINT,
+                expectedLiveMetricsEndpoint: Constants.DEFAULT_LIVEMETRICS_ENDPOINT
+            });
+        });
+
+        it("should use correct endpoints when using EndpointSuffix", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com",
+                expectedBreezeEndpoint: "https://dc.ai.contoso.com",
+                expectedLiveMetricsEndpoint: "https://live.ai.contoso.com"
+            });
+        });
+
+        it("should use correct endpoints when using EndpointSuffix with explicit override", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;LiveEndpoint=https://custom.live.contoso.com:444",
+                expectedBreezeEndpoint: "https://dc.ai.contoso.com",
+                expectedLiveMetricsEndpoint: "https://custom.live.contoso.com:444"
+            });
+        });
+
+        it("should parse EndpointSuffix + Location", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;Location=westus2",
+                expectedBreezeEndpoint: "https://westus2.dc.ai.contoso.com",
+                expectedLiveMetricsEndpoint: "https://westus2.live.ai.contoso.com"
+            });
+        });
+
+        it("should parse EndpointSuffix + Location + Endpoint Override", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;Location=westus2;LiveEndpoint=https://custom.contoso.com:444",
+                expectedBreezeEndpoint: "https://westus2.dc.ai.contoso.com",
+                expectedLiveMetricsEndpoint: "https://custom.contoso.com:444"
+            });
+        });
+
+        it("should parse Endpoint Override", () => {
+            runTest({
+                connectionString: "InstrumentationKey=00000000-0000-0000-0000-000000000000;LiveEndpoint=http://custom.live.endpoint.com:444",
+                expectedBreezeEndpoint: Constants.DEFAULT_BREEZE_ENDPOINT,
+                expectedLiveMetricsEndpoint: "http://custom.live.endpoint.com:444"
+            });
+        });
+    });
+});