Parse appId from Breeze reponse and pass it as Request-Context (#575)

* Parse appId from Breeze reponse and pass it as Request-Context

* Fix unit tests

* Exposing appId in the config
This commit is contained in:
YuliaSafarova 2018-03-22 17:16:29 -07:00 коммит произвёл GitHub
Родитель f43350b03a
Коммит 0c1baa3ea8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 262 добавлений и 19 удалений

Просмотреть файл

@ -315,8 +315,9 @@ interface IConfig {
// Default: false
isStorageUseDisabled: boolean;
// Default true. If false, the SDK will add two headers ('x-ms-request-root-id' and 'x-ms-request-id)
// to all dependency requests (within the same domain) to correlate them with corresponding requests on the server side.
// If false, the SDK will add two headers ('Request-Id' and 'Request-Context') to all
// dependency requests to correlate them with corresponding requests on the server side.
// Default false.
disableCorrelationHeaders: boolean;
// If true, the SDK will send all telemetry using [Beacon API](https://www.w3.org/TR/beacon/)
@ -332,6 +333,11 @@ interface IConfig {
// If true, the SDK will track all [Browser Link](https://docs.microsoft.com/en-us/aspnet/core/client-side/using-browserlink) requests.
// Default: false
isBrowserLinkTrackingEnabled: boolean;
// AppId is used for the correlation between AJAX dependencies happening on the client-side with the server-side requests.
// When Beacon API is enabled it can not be used automatically, but can be set manually in the configuration.
// Default: null
appId: string;
}
```

Просмотреть файл

@ -34,5 +34,6 @@
isBeaconApiDisabled?: boolean;
sdkExtension?: string;
isBrowserLinkTrackingEnabled?: boolean;
appId?: string;
}
}

Просмотреть файл

@ -516,7 +516,8 @@ class SessionContextTests extends TestClass {
isRetryDisabled: () => null,
isBeaconApiDisabled: () => null,
sdkExtension: () => null,
isBrowserLinkTrackingEnabled: () => null
isBrowserLinkTrackingEnabled: () => null,
appId: () => null,
};
}
}

Просмотреть файл

@ -413,7 +413,8 @@ class UserContextTests extends TestClass {
isRetryDisabled: () => null,
isBeaconApiDisabled: () => null,
sdkExtension: () => null,
isBrowserLinkTrackingEnabled: () => null
isBrowserLinkTrackingEnabled: () => null,
appId: () => null,
};
}
}

Просмотреть файл

@ -162,7 +162,7 @@ class SenderTests extends TestClass {
// verify
this.requestAsserts();
this.fakeServer.requests.pop().respond(200, { "Content-Type": "application/json" }, '{"test":"success"}');
this.fakeServer.requests.pop().respond(200, { "Content-Type": "application/json" }, '{"itemsReceived": 1, "itemsAccepted": 1, "errors": []}');
this.successAsserts(sender);
this.logAsserts(0);
sender.successSpy.reset();
@ -175,9 +175,9 @@ class SenderTests extends TestClass {
// verify
this.requestAsserts();
this.fakeServer.requests.pop().respond(404, { "Content-Type": "application/json" }, 'some_error');
this.fakeServer.requests[0].respond(404, { "Content-Type": "application/json" }, '{"itemsReceived": 1, "itemsAccepted": 0, "errors": [{ "index": 0, "statusCode": 404, "message": "Not found" }]}');
this.errorAsserts(sender);
this.logAsserts(1, "message:XMLHttpRequest,Status:404,Response:some_error");
this.logAsserts(1, "message:XMLHttpRequest,Status:404");
sender.successSpy.reset();
sender.errorSpy.reset();
}
@ -228,9 +228,9 @@ class SenderTests extends TestClass {
// verify
this.requestAsserts();
this.fakeServer.requests[0].respond(404, { "Content-Type": "application/json" }, '400');
this.fakeServer.requests[0].respond(404, { "Content-Type": "application/json" }, '{ "itemsReceived": 1, "itemsAccepted": 0, "errors": [{ "index": 0, "statusCode": 404, "message": "Not found" }]}');
this.errorAsserts(sender);
this.logAsserts(1, "message:XDomainRequest,Response:400");
this.logAsserts(1, "message:partial success 0 of 1");
sender.successSpy.reset();
sender.errorSpy.reset();
}
@ -725,6 +725,84 @@ class SenderTests extends TestClass {
}
});
this.testCase({
name: "SenderTests: XMLHttpRequest sender successfully parses appId from the response.",
test: () => {
// setup
XMLHttpRequest = <any>(() => {
var xhr = new this.xhr;
xhr.withCredentials = false;
return xhr;
});
var sender = this.getSender();
Assert.ok(sender, "sender was constructed. Testing response code: 200");
this.fakeServer.requests.pop();
sender.send(this.testTelemetry);
Assert.equal(1, sender._buffer.count(), "Buffer has one item");
// trigger send
this.clock.tick(sender._config.maxBatchInterval());
this.requestAsserts();
this.fakeServer.requests.pop().respond(
200,
{ "Content-Type": "application/json" },
'{ "itemsReceived": 1, "itemsAccepted": 1, "errors": [], "appId": "C16FBA4D-ECE9-472E-8125-4FF5BEFAF8C1" }'
);
// verify
Assert.ok(sender.successSpy.called, "success was invoked");
Assert.ok(sender.errorSpy.notCalled, "error was not invoked");
Assert.equal("C16FBA4D-ECE9-472E-8125-4FF5BEFAF8C1", sender._appId, "App Id was parsed.");
// clean up
this.testCleanup();
}
});
this.testCase({
name: "SenderTests: XMLHttpRequest sender does not store appId from the response if it's not returned.",
test: () => {
// setup
XMLHttpRequest = <any>(() => {
var xhr = new this.xhr;
xhr.withCredentials = false;
return xhr;
});
var sender = this.getSender();
Assert.ok(sender, "sender was constructed. Testing response code: 200");
this.fakeServer.requests.pop();
sender.send(this.testTelemetry);
Assert.equal(1, sender._buffer.count(), "Buffer has one item");
// trigger send
this.clock.tick(sender._config.maxBatchInterval());
this.requestAsserts();
this.fakeServer.requests.pop().respond(
200,
{ "Content-Type": "application/json" },
'{ "itemsReceived": 1, "itemsAccepted": 1, "errors": [] }'
);
// verify
Assert.ok(sender.successSpy.called, "success was invoked");
Assert.ok(sender.errorSpy.notCalled, "error was not invoked");
Assert.equal(null, sender._appId, "App Id was not parsed.");
// clean up
this.testCleanup();
}
});
this.testCase({
name: "SenderTests: XMLHttpRequest sender does NOT retry on non-retriable response code from the backend.",
test: () => {

Просмотреть файл

@ -26,7 +26,8 @@ class TelemetryContextTests extends TestClass {
isRetryDisabled: () => false,
isBeaconApiDisabled: () => true,
sdkExtension: () => null,
isBrowserLinkTrackingEnabled: () => false
isBrowserLinkTrackingEnabled: () => false,
appId: () => undefined,
}
this._telemetryContext = new Microsoft.ApplicationInsights.TelemetryContext(this._config);

Просмотреть файл

@ -11,7 +11,8 @@ class AjaxTests extends TestClass {
context: {
operation: {
id: "asdf"
}
},
appId: () => "someid"
},
config: {
disableCorrelationHeaders: false

Просмотреть файл

@ -31,7 +31,8 @@ class AppInsightsTests extends TestClass {
isCookieUseDisabled: false,
isRetryDisabled: false,
isStorageUseDisabled: false,
isBeaconApiDisabled: true
isBeaconApiDisabled: true,
appId: undefined
};
// set default values
@ -175,6 +176,28 @@ class AppInsightsTests extends TestClass {
}
});
this.testCase({
name: "AppInsightsTests: appId is propagated from the config",
test: () => {
var expectedAppId = "BDC8736D-D8E8-4B69-B19B-B0CE6B66A456";
// setup
var config = this.getAppInsightsSnippet();
config.appId = expectedAppId;
var appInsights = new Microsoft.ApplicationInsights.AppInsights(config);
var trackStub = this.sandbox.stub(appInsights.context._sender, "send");
this.clock.tick(60000);
Assert.equal(expectedAppId, appInsights.context._sender._appId);
// act
appInsights.trackEvent("testEvent");
// verify
Assert.equal(expectedAppId, appInsights.context._sender._appId);
}
});
this.testCase({
name: "AppInsightsTests: application context is applied",
test: () => {
@ -1710,13 +1733,14 @@ class AppInsightsTests extends TestClass {
});
this.testCase({
name: "Ajax - Request-Id is set and passed correctly",
name: "Ajax - Request-Context is not set if appId was not set",
test: () => {
var snippet = this.getAppInsightsSnippet();
snippet.disableAjaxTracking = false;
snippet.disableCorrelationHeaders = false;
snippet.maxBatchInterval = 0;
var appInsights = new Microsoft.ApplicationInsights.AppInsights(snippet);
var trackStub = this.sandbox.spy(appInsights, "trackDependencyData");
var expectedRootId = appInsights.context.operation.id;
Assert.ok(expectedRootId.length > 0, "root id was initialized to non empty string");
@ -1733,7 +1757,40 @@ class AppInsightsTests extends TestClass {
(<any>xhr).respond("200", {}, "");
// Assert
Assert.equal(expectedAjaxId, (<any>xhr).requestHeaders['Request-Id'], "Request-Id id set correctly");
Assert.equal(expectedAjaxId, (<any>xhr).requestHeaders['Request-Id'], "Request-Id is set correctly");
Assert.equal(null, (<any>xhr).requestHeaders['Request-Context'], "Request-Context is not set");
Assert.equal(expectedAjaxId, trackStub.args[0][0].id, "ajax id passed to trackAjax correctly");
}
});
this.testCase({
name: "Ajax - Request-Id and Request-Context are set and passed correctly",
test: () => {
var snippet = this.getAppInsightsSnippet();
snippet.disableAjaxTracking = false;
snippet.disableCorrelationHeaders = false;
snippet.maxBatchInterval = 0;
var appInsights = new Microsoft.ApplicationInsights.AppInsights(snippet);
appInsights.context.appId = () => "C16FBA4D-ECE9-472E-8125-4FF5BEFAF8C1";
var trackStub = this.sandbox.spy(appInsights, "trackDependencyData");
var expectedRootId = appInsights.context.operation.id;
Assert.ok(expectedRootId.length > 0, "root id was initialized to non empty string");
// Act
var xhr = new XMLHttpRequest();
xhr.open("GET", "/bla");
xhr.send();
var expectedAjaxId = (<any>xhr).ajaxData.id;
Assert.ok(expectedAjaxId.length > 0, "ajax id was initialized");
// Emulate response
(<any>xhr).respond("200", {}, "");
// Assert
Assert.equal(expectedAjaxId, (<any>xhr).requestHeaders['Request-Id'], "Request-Id is set correctly");
Assert.equal("appId=cid-v1:C16FBA4D-ECE9-472E-8125-4FF5BEFAF8C1", (<any>xhr).requestHeaders['Request-Context'], "Request-Context is set correctly");
Assert.equal(expectedAjaxId, trackStub.args[0][0].id, "ajax id passed to trackAjax correctly");
}
});
@ -1772,13 +1829,16 @@ class AppInsightsTests extends TestClass {
});
this.testCase({
name: "Ajax - disableCorrelationHeaders disables Request-Id header",
name: "Ajax - disableCorrelationHeaders disables Request-Id and Request-Context headers",
test: () => {
var snippet = this.getAppInsightsSnippet();
snippet.disableAjaxTracking = false;
snippet.disableCorrelationHeaders = true;
snippet.maxBatchInterval = 0;
var appInsights = new Microsoft.ApplicationInsights.AppInsights(snippet);
appInsights.context.appId = () => "C16FBA4D-ECE9-472E-8125-4FF5BEFAF8C1";
var trackStub = this.sandbox.spy(appInsights, "trackDependencyData");
var expectedRootId = appInsights.context.operation.id;
Assert.ok(expectedRootId.length > 0, "root id was initialized to non empty string");
@ -1793,11 +1853,12 @@ class AppInsightsTests extends TestClass {
// Assert
Assert.equal(null, (<any>xhr).requestHeaders['Request-Id'], "Request-Id header is not set.");
Assert.equal(null, (<any>xhr).requestHeaders['Request-Context'], "Request-Context header is not set.");
}
});
this.testCase({
name: "Ajax - Request-Id header is disabled for excluded domain",
name: "Ajax - Request-Id and Request-Context headers are disabled for excluded domain",
test: () => {
var snippet = this.getAppInsightsSnippet();
snippet.disableAjaxTracking = false;
@ -1819,6 +1880,7 @@ class AppInsightsTests extends TestClass {
// Assert
Assert.equal(null, (<any>xhr).requestHeaders['Request-Id'], "Request-Id header is not set.");
Assert.equal(null, (<any>xhr).requestHeaders['Request-Context'], "Request-Context header is not set.");
}
});
}

Просмотреть файл

@ -1272,10 +1272,23 @@ declare module Microsoft.ApplicationInsights {
* calling application when processing incoming responses.
*/
static requestContextTargetKey: string;
/**
* Request-Context appId format
*/
static requestContextAppIdFormat: string;
/**
* Request-Id header
*/
static requestIdHeader: string;
/**
* Sdk-Context header
* If this header passed with appId in content then appId will be returned back by the backend.
*/
static sdkContextHeader: string;
/**
* String to pass in header for requesting appId back from the backend.
*/
static sdkContextHeaderAppIdRequest: string;
}
}
declare module Microsoft.Telemetry {
@ -1613,6 +1626,10 @@ declare module Microsoft.ApplicationInsights {
* List of errors for items which were not accepted
*/
errors: IResponseError[];
/**
* App id returned by the backend - not necessary returned, but we don't need it with each response.
*/
appId?: string;
}
class Sender {
/**
@ -1639,6 +1656,10 @@ declare module Microsoft.ApplicationInsights {
* The configuration for this sender instance
*/
_config: ISenderConfig;
/**
* AppId of this component parsed from some backend response.
*/
_appId: string;
/**
* A method which will cause data to be send to the url
*/
@ -1708,12 +1729,16 @@ declare module Microsoft.ApplicationInsights {
*
* Note: XDomainRequest does not support sync requests. This 'isAsync' parameter is added
* to maintain consistency with the xhrSender's contract
* Note: XDomainRequest does not support custom headers and we are not able to get
* appId from the backend for the correct correlation.
*/
private _xdrSender(payload, isAsync);
/**
* Send Beacon API request
* @param payload {string} - The data payload to be sent.
* @param isAsync {boolean} - not used
* Note: Beacon API does not support custom headers and we are not able to get
* appId from the backend for the correct correlation.
*/
private _beaconSender(payload, isAsync);
/**
@ -2223,6 +2248,7 @@ declare module Microsoft.ApplicationInsights {
cookieDomain: () => string;
sdkExtension: () => string;
isBrowserLinkTrackingEnabled: () => boolean;
appId: () => string;
}
class TelemetryContext implements ITelemetryContext {
/**
@ -2259,6 +2285,10 @@ declare module Microsoft.ApplicationInsights {
* The object describing a session tracked by this object.
*/
session: Context.Session;
/**
* AppId of this component if returned by the backend.
*/
appId: () => string;
/**
* The array of telemetry initializers to call before sending each telemetry item.
*/
@ -2426,6 +2456,7 @@ declare module Microsoft.ApplicationInsights {
isBeaconApiDisabled?: boolean;
sdkExtension?: string;
isBrowserLinkTrackingEnabled?: boolean;
appId?: string;
}
}
declare module Microsoft.ApplicationInsights {

Просмотреть файл

@ -85,7 +85,8 @@ module Microsoft.ApplicationInsights {
isRetryDisabled: () => this.config.isRetryDisabled,
isBeaconApiDisabled: () => this.config.isBeaconApiDisabled,
sdkExtension: () => this.config.sdkExtension,
isBrowserLinkTrackingEnabled: () => this.config.isBrowserLinkTrackingEnabled
isBrowserLinkTrackingEnabled: () => this.config.isBrowserLinkTrackingEnabled,
appId: () => this.config.appId,
}
if (this.config.isCookieUseDisabled) {

Просмотреть файл

@ -5,7 +5,7 @@ module Microsoft.ApplicationInsights {
/**
* Request-Context header
*/
public static requestContextHeader = "request-context";
public static requestContextHeader = "Request-Context";
/**
* Target instrumentation header that is added to the response and retrieved by the
@ -13,9 +13,25 @@ module Microsoft.ApplicationInsights {
*/
public static requestContextTargetKey = "appId";
/**
* Request-Context appId format
*/
public static requestContextAppIdFormat = "appId=cid-v1:";
/**
* Request-Id header
*/
public static requestIdHeader = "Request-Id";
/**
* Sdk-Context header
* If this header passed with appId in content then appId will be returned back by the backend.
*/
public static sdkContextHeader = "Sdk-Context";
/**
* String to pass in header for requesting appId back from the backend.
*/
public static sdkContextHeaderAppIdRequest = "appId";
}
}

Просмотреть файл

@ -89,6 +89,11 @@ module Microsoft.ApplicationInsights {
* List of errors for items which were not accepted
*/
errors: IResponseError[];
/**
* App id returned by the backend - not necessary returned, but we don't need it with each response.
*/
appId?: string;
}
export class Sender {
@ -122,6 +127,11 @@ module Microsoft.ApplicationInsights {
*/
public _config: ISenderConfig;
/**
* AppId of this component parsed from some backend response.
*/
public _appId: string;
/**
* A method which will cause data to be send to the url
*/
@ -388,6 +398,7 @@ module Microsoft.ApplicationInsights {
xhr[AjaxMonitor.DisabledPropertyName] = true;
xhr.open("POST", this._config.endpointUrl(), isAsync);
xhr.setRequestHeader("Content-type", "application/json");
xhr.setRequestHeader(RequestHeaders.sdkContextHeader, RequestHeaders.sdkContextHeaderAppIdRequest);
xhr.onreadystatechange = () => this._xhrReadyStateChange(xhr, payload, payload.length);
xhr.onerror = (event: ErrorEvent) => this._onError(payload, this._formatErrorMessageXhr(xhr), event);
@ -405,6 +416,8 @@ module Microsoft.ApplicationInsights {
*
* Note: XDomainRequest does not support sync requests. This 'isAsync' parameter is added
* to maintain consistency with the xhrSender's contract
* Note: XDomainRequest does not support custom headers and we are not able to get
* appId from the backend for the correct correlation.
*/
private _xdrSender(payload: string[], isAsync: boolean) {
var xdr = new XDomainRequest();
@ -438,6 +451,8 @@ module Microsoft.ApplicationInsights {
* Send Beacon API request
* @param payload {string} - The data payload to be sent.
* @param isAsync {boolean} - not used
* Note: Beacon API does not support custom headers and we are not able to get
* appId from the backend for the correct correlation.
*/
private _beaconSender(payload: string[], isAsync: boolean) {
var url = this._config.endpointUrl();
@ -462,6 +477,14 @@ module Microsoft.ApplicationInsights {
*/
public _xhrReadyStateChange(xhr: XMLHttpRequest, payload: string[], countOfItemsInPayload: number) {
if (xhr.readyState === 4) {
var response: IBackendResponse = null;
if (!this._appId) {
response = this._parseResponse(xhr.responseText || xhr.response);
if (response && response.appId) {
this._appId = response.appId;
}
}
if ((xhr.status < 200 || xhr.status >= 300) && xhr.status !== 0) {
if (!this._config.isRetryDisabled() && this._isRetriable(xhr.status)) {
this._resendPayload(payload);
@ -475,7 +498,9 @@ module Microsoft.ApplicationInsights {
}
} else {
if (xhr.status === 206) {
var response = this._parseResponse(xhr.responseText || xhr.response);
if (!response) {
response = this._parseResponse(xhr.responseText || xhr.response);
}
if (response && !this._config.isRetryDisabled()) {
this._onPartialSuccess(payload, response);

Просмотреть файл

@ -20,6 +20,7 @@ module Microsoft.ApplicationInsights {
cookieDomain: () => string;
sdkExtension: () => string;
isBrowserLinkTrackingEnabled: () => boolean;
appId: () => string;
}
export class TelemetryContext implements ITelemetryContext {
@ -67,6 +68,11 @@ module Microsoft.ApplicationInsights {
*/
public session: Context.Session;
/**
* AppId of this component if returned by the backend.
*/
public appId: () => string;
/**
* The array of telemetry initializers to call before sending each telemetry item.
*/
@ -80,6 +86,13 @@ module Microsoft.ApplicationInsights {
constructor(config: ITelemetryConfig) {
this._config = config;
this._sender = new Sender(config);
this.appId = () => this._sender._appId;
// use appId set in config instead of getting it from the backend
if (config.appId()) {
this._sender._appId = config.appId();
}
this.telemetryInitializers = [];
// window will be undefined in node.js where we do not want to initialize contexts

Просмотреть файл

@ -159,6 +159,12 @@ module Microsoft.ApplicationInsights {
if (CorrelationIdHelper.canIncludeCorrelationHeader(this.appInsights.config, xhr.ajaxData.getAbsoluteUrl())) {
xhr.setRequestHeader(RequestHeaders.requestIdHeader, xhr.ajaxData.id);
if (this.appInsights.context) {
var appId = this.appInsights.context.appId();
if (appId) {
xhr.setRequestHeader(RequestHeaders.requestContextHeader, RequestHeaders.requestContextAppIdFormat + appId);
}
}
}
xhr.ajaxData.xhrMonitoringState.sendDone = true;
}