Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to enrich dependencies logs with context at the beginning of api call #1624

Merged
merged 11 commits into from
Aug 6, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Most configuration fields are named such that they can be defaulted to falsey. A
| disableAjaxTracking | boolean | false | If true, Ajax calls are not autocollected. Default is false. |
| disableFetchTracking | boolean | true | If true, Fetch requests are not autocollected. Default is true |
| excludeRequestFromAutoTrackingPatterns | string[] \| RegExp[] | undefined | Provide a way to exclude specific route from automatic tracking for XMLHttpRequest or Fetch request. If defined, for an ajax / fetch request that the request url matches with the regex patterns, auto tracking is turned off. Default is undefined. |
| addRequestContext | (requestContext: IRequestionContext) => {[key: string]: any} | undefined | Provide a way to enrich dependencies logs with context at the beginning of api call. Default is undefined. You will need to check if `xhr` exists if you configure `xhr` related conetext. You will need to check if `fetch request` and `fetch response` exist if you configure `fetch` related context. Otherwise you may not get the data you need. |
| overridePageViewDuration | boolean | false | If true, default behavior of trackPageView is changed to record end of page view duration interval when trackPageView is called. If false and no custom duration is provided to trackPageView, the page view performance is calculated using the navigation timing API. Default is false. |
| maxAjaxCallsPerView | numeric | 500 | Default 500 - controls how many ajax calls will be monitored per page view. Set to -1 to monitor all (unlimited) ajax calls on the page. |
| disableDataLossAnalysis | boolean | true | If false, internal telemetry sender buffers will be checked at startup for items not yet sent. |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Assert, AITestClass, PollingAssert } from "@microsoft/ai-test-framework";
import { AjaxMonitor } from "../../../src/ajax";
import { DisabledPropertyName, IConfig, DistributedTracingModes, RequestHeaders, IDependencyTelemetry } from "@microsoft/applicationinsights-common";
import { DisabledPropertyName, IConfig, DistributedTracingModes, RequestHeaders, IDependencyTelemetry, IRequestContext } from "@microsoft/applicationinsights-common";
import {
AppInsightsCore, IConfiguration, ITelemetryItem, ITelemetryPlugin, IChannelControls, _InternalMessageId,
getPerformance, getGlobalInst, getGlobal
Expand Down Expand Up @@ -238,6 +238,45 @@ export class AjaxTests extends AITestClass {
}
});

this.testCase({
name: "Ajax: add context into custom dimension with call back configuration on AI initialization.",
test: () => {
this._ajax = new AjaxMonitor();
let dependencyFields = hookTrackDependencyInternal(this._ajax);
let appInsightsCore = new AppInsightsCore();
var trackStub = this.sandbox.stub(appInsightsCore, "track");

let coreConfig: IConfiguration & IConfig = {
instrumentationKey: "",
disableAjaxTracking: false,
addRequestContext: (requestContext: IRequestContext) => {
return {
test: "ajax context",
xhrStatus: requestContext.status
}
}
};
appInsightsCore.initialize(coreConfig, [this._ajax, new TestChannelPlugin()]);

// act
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://microsoft.com");
xhr.send();

// Emulate response
(<any>xhr).respond(200, {}, "");

Assert.equal(1, dependencyFields.length, "trackDependencyDataInternal was called");

// assert
Assert.ok(trackStub.calledOnce, "track is called");
let data = trackStub.args[0][0].baseData;
Assert.equal("Ajax", data.type, "request is Ajax type");
Assert.equal("ajax context", data.properties.test, "xhr request's request context is added when customer configures addRequestContext.");
Assert.equal(200, data.properties.xhrStatus, "xhr object properties are captured");
}
});

this.testCase({
name: "Ajax: xhr request header is tracked as part C data when enableRequestHeaderTracking flag is true",
test: () => {
Expand Down Expand Up @@ -483,6 +522,66 @@ export class AjaxTests extends AITestClass {
}]
});

this.testCaseAsync({
name: "Fetch: add context into custom dimension with call back configuration on AI initialization.",
stepDelay: 10,
autoComplete: false,
timeOut: 10000,
steps: [ (testContext) => {
hookFetch((resolve) => {
AITestClass.orgSetTimeout(function() {
resolve({
headers: new Headers(),
ok: true,
body: null,
bodyUsed: false,
redirected: false,
status: 200,
statusText: "Hello",
trailer: null,
type: "basic",
url: "https://httpbin.org/status/200"
});
}, 0);
});

this._ajax = new AjaxMonitor();
let dependencyFields = hookTrackDependencyInternal(this._ajax);
let appInsightsCore = new AppInsightsCore();
let coreConfig = {
instrumentationKey: "",
disableFetchTracking: false,
addRequestContext: (requestContext: IRequestContext) => {
return {
test: "Fetch context",
fetchRequestUrl: requestContext.request,
fetchResponseType: (requestContext.response as Response).type
}
}
};
appInsightsCore.initialize(coreConfig, [this._ajax, new TestChannelPlugin()]);
let fetchSpy = this.sandbox.spy(appInsightsCore, "track")

// Act
Assert.ok(fetchSpy.notCalled, "No fetch called yet");
fetch("https://httpbin.org/status/200", {method: "post", [DisabledPropertyName]: false}).then(() => {
// assert
Assert.ok(fetchSpy.calledOnce, "track is called");
let data = fetchSpy.args[0][0].baseData;
Assert.equal("Fetch", data.type, "request is Fetch type");
Assert.equal(1, dependencyFields.length, "trackDependencyDataInternal was called");
Assert.equal("Fetch context", data.properties.test, "Fetch request's request context is added when customer configures addRequestContext.");
Assert.equal("https://httpbin.org/status/200", data.properties.fetchRequestUrl, "Fetch request is captured.");
Assert.equal("basic", data.properties.fetchResponseType, "Fetch response is captured.");

testContext.testDone();
}, () => {
Assert.ok(false, "fetch failed!");
testContext.testDone();
});
}]
});

this.testCaseAsync({
name: "Fetch: fetch gets instrumented",
stepDelay: 10,
Expand Down
46 changes: 39 additions & 7 deletions extensions/applicationinsights-dependencies-js/src/ajax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import {
RequestHeaders, CorrelationIdHelper, TelemetryItemCreator, ICorrelationConfig,
RemoteDependencyData, dateTimeUtilsNow, DisabledPropertyName, IDependencyTelemetry,
IConfig, ITelemetryContext, PropertiesPluginIdentifier, DistributedTracingModes
IConfig, ITelemetryContext, PropertiesPluginIdentifier, DistributedTracingModes, IRequestContext
} from '@microsoft/applicationinsights-common';
import {
isNullOrUndefined, arrForEach, isString, strTrim, isFunction, LoggingSeverity, _InternalMessageId,
IAppInsightsCore, BaseTelemetryPlugin, ITelemetryPluginChain, IConfiguration, IPlugin, ITelemetryItem, IProcessTelemetryContext,
getLocation, getGlobal, strUndefined, strPrototype, IInstrumentCallDetails, InstrumentFunc, InstrumentProto, getPerformance,
IInstrumentHooksCallbacks, IInstrumentHook, objForEachKey, generateW3CId, getIEVersion, dumpObj,objKeys
IInstrumentHooksCallbacks, IInstrumentHook, objForEachKey, generateW3CId, getIEVersion, dumpObj,objKeys, ICustomProperties
} from '@microsoft/applicationinsights-core-js';
import { ajaxRecord, IAjaxRecordResponse } from './ajaxRecord';
import { EventHelper } from './ajaxUtils';
Expand Down Expand Up @@ -176,7 +176,8 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
ignoreHeaders:[
"Authorization",
"X-API-Key",
"WWW-Authenticate"]
"WWW-Authenticate"],
addRequestContext: undefined
}
return config;
}
Expand Down Expand Up @@ -214,6 +215,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
let _hooks:IInstrumentHook[] = [];
let _disabledUrls:any = {};
let _excludeRequestFromAutoTrackingPatterns: string[] | RegExp[];
let _addRequestContext: (requestContext?: IRequestContext) => ICustomProperties;

dynamicProto(AjaxMonitor, this, (_self, base) => {
_self.initialize = (config: IConfiguration & IConfig, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?:ITelemetryPluginChain) => {
Expand All @@ -231,6 +233,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
_maxAjaxCallsPerView = _config.maxAjaxCallsPerView;
_enableResponseHeaderTracking = _config.enableResponseHeaderTracking;
_excludeRequestFromAutoTrackingPatterns = _config.excludeRequestFromAutoTrackingPatterns;
_addRequestContext = _config.addRequestContext;

_isUsingAIHeaders = distributedTracingMode === DistributedTracingModes.AI || distributedTracingMode === DistributedTracingModes.AI_AND_W3C;
_isUsingW3CHeaders = distributedTracingMode === DistributedTracingModes.AI_AND_W3C || distributedTracingMode === DistributedTracingModes.W3C;
Expand Down Expand Up @@ -425,7 +428,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
if (fetchData) {
// Replace the result with the new promise from this code
callDetails.rslt = callDetails.rslt.then((response: any) => {
_reportFetchMetrics(callDetails, (response||{}).status, response, fetchData, () => {
_reportFetchMetrics(callDetails, (response||{}).status, input, response, fetchData, () => {
let ajaxResponse:IAjaxRecordResponse = {
statusText: response.statusText,
headerMap: null,
Expand All @@ -447,7 +450,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
return response;
})
.catch((reason: any) => {
_reportFetchMetrics(callDetails, 0, input, fetchData, null, { error: reason.message });
_reportFetchMetrics(callDetails, 0, input, null, fetchData, null, { error: reason.message });
throw reason;
});
}
Expand Down Expand Up @@ -724,7 +727,21 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
return ajaxResponse;
});

let properties;
try {
if (!!_addRequestContext) {
properties = _addRequestContext({status: xhr.status, xhr});
}
} catch (e) {
_throwInternalWarning(_self,
_InternalMessageId.FailedAddingCustomDefinedRequestContext,
"Failed to add custom defined request context as configured call back may missing a null check.")
}

if (dependency) {
if (properties !== undefined) {
dependency.properties = {...dependency.properties, ...properties};
}
_self[strTrackDependencyDataInternal](dependency);
} else {
_reportXhrError(null, {
Expand Down Expand Up @@ -900,7 +917,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
return result;
}

function _reportFetchMetrics(callDetails: IInstrumentCallDetails, status: number, input: Request | Response | string, ajaxData: ajaxRecord, getResponse:() => IAjaxRecordResponse, properties?: { [key: string]: any }): void {
function _reportFetchMetrics(callDetails: IInstrumentCallDetails, status: number, input: Request, response: Response | string, ajaxData: ajaxRecord, getResponse:() => IAjaxRecordResponse, properties?: { [key: string]: any }): void {
if (!ajaxData) {
return;
}
Expand All @@ -923,7 +940,22 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu

_findPerfResourceEntry("fetch", ajaxData, () => {
const dependency = ajaxData.CreateTrackItem("Fetch", _enableRequestHeaderTracking, getResponse);

let properties;
try {
if (!!_addRequestContext) {
properties = _addRequestContext({status, request: input, response});
}
} catch (e) {
_throwInternalWarning(_self,
_InternalMessageId.FailedAddingCustomDefinedRequestContext,
"Failed to add custom defined request context as configured call back may missing a null check.")
}

if (dependency) {
if (properties !== undefined) {
dependency.properties = {...dependency.properties, ...properties};
}
_self[strTrackDependencyDataInternal](dependency);
} else {
_reportFetchError(_InternalMessageId.FailedMonitorAjaxDur, null,
Expand Down Expand Up @@ -981,7 +1013,7 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu
}

/**
* Protected function to allow sub classes the chance to add additional properties to the delendency event
* Protected function to allow sub classes the chance to add additional properties to the dependency event
* before it's sent. This function calls track, so sub-classes must call this function after they have
* populated their properties.
* @param dependencyData dependency data object
Expand Down
9 changes: 8 additions & 1 deletion shared/AppInsightsCommon/src/Interfaces/IConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { IConfiguration, ICookieMgrConfig, isNullOrUndefined } from '@microsoft/applicationinsights-core-js';
import { IConfiguration, ICookieMgrConfig, isNullOrUndefined, ICustomProperties } from '@microsoft/applicationinsights-core-js';
import { DistributedTracingModes } from '../Enums';
import { IRequestContext } from './IRequestContext';

/**
* @description Configuration settings for how telemetry is sent
Expand Down Expand Up @@ -126,6 +127,12 @@ export interface IConfig {
*/
excludeRequestFromAutoTrackingPatterns?: string[] | RegExp[];

/**
* Provide a way to enrich dependencies logs with context at the beginning of api call.
* Default is undefined.
*/
addRequestContext?: (requestContext?: IRequestContext) => ICustomProperties;

/**
* @description If true, default behavior of trackPageView is changed to record end of page view duration interval when trackPageView is called. If false and no custom duration is provided to trackPageView, the page view performance is calculated using the navigation timing API. Default is false
* @type {boolean}
Expand Down
8 changes: 8 additions & 0 deletions shared/AppInsightsCommon/src/Interfaces/ICorrelationConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { ICustomProperties } from '@microsoft/applicationinsights-core-js';
import { DistributedTracingModes } from '../Enums';
import { IRequestContext } from './IRequestContext';

export interface ICorrelationConfig {
enableCorsCorrelation: boolean;
Expand Down Expand Up @@ -52,4 +54,10 @@ export interface ICorrelationConfig {
* Default is undefined.
*/
excludeRequestFromAutoTrackingPatterns?: string[] | RegExp[];

/**
* Provide a way to enrich dependencies logs with context at the beginning of api call.
* Default is undefined.
*/
addRequestContext?: (requestContext?: IRequestContext) => ICustomProperties;
}
6 changes: 6 additions & 0 deletions shared/AppInsightsCommon/src/Interfaces/IRequestContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface IRequestContext {
status?: number;
xhr?: XMLHttpRequest;
request?: Request; // fetch request
response?: Response | string; // fetch response
};
1 change: 1 addition & 0 deletions shared/AppInsightsCommon/src/applicationinsights-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export { IOperatingSystem } from './Interfaces/Context/IOperatingSystem';
export { IPropertiesPlugin } from './Interfaces/IPropertiesPlugin';
export { IUser, IUserContext } from './Interfaces/Context/IUser';
export { ITelemetryTrace, ITraceState } from './Interfaces/Context/ITelemetryTrace';
export { IRequestContext } from './Interfaces/IRequestContext';
export { DistributedTracingModes } from './Enums';
export { stringToBoolOrDefault, msToTimeSpan, isBeaconApiSupported, getExtensionByName, isCrossOriginError } from './HelperFuncs';
export { createDomEvent } from './DomHelperFuncs';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const _InternalMessageId = {
InvalidInstrumentationKey:100,
CannotParseAiBlobValue: 101,
InvalidContentBlob: 102,
TrackPageActionEventFailed: 103
TrackPageActionEventFailed: 103,
FailedAddingCustomDefinedRequestContext: 104
};
export type _InternalMessageId = number | typeof _InternalMessageId;