Feature: add SPA auto route change tracking (#947)

* add spa auto route change tracking

* use this.config isntead of config

* readme: remove newly unneeded documenation

* analyics: add pushState undefined tests

* types: add properties plugin types

* tests: add any type to tests

* send pv duration of 0, refresh operation name

* analytics: use self ref to this inside callbacks
This commit is contained in:
Mark Wolff 2019-06-27 11:59:53 -07:00 коммит произвёл GitHub
Родитель 3d92a13481
Коммит c2e413decf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 149 добавлений и 14 удалений

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

@ -154,16 +154,13 @@ Most configuration fields are named such that they can be defaulted to falsey. A
| appId | null | 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 cannot be used automatically, but can be set manually in the configuration. Default is null |
| enableCorsCorrelation | false | If true, the SDK will add two headers ('Request-Id' and 'Request-Context') to all CORS requests tocorrelate outgoing AJAX dependencies with corresponding requests on the server side. Default is false |
| namePrefix | undefined | An optional value that will be used as name postfix for localStorage and cookie name.
<!-- | enableAutoRouteTracking | false | Automatically track route changes in Single Page Applications (SPA). If true, each route change will send a new Pageview to Application Insights. Hash route changes changes (`example.com/foo#bar`) are also recorded as new page views. -->
## Single Page Applications
By default, this SDK will **not** handle state based route changing that occurs in single page applications unless you use a plugin designed for your frontend framework (Angular, React, Vue, etc). Currently, we support a separate [React plugin](#available-extensions-for-the-sdk) which you can initialize with this SDK and it will accomplish this for you in your React application. Otherwise, you must manually trigger pageviews on each route change. An example of accomplishing this for a React application is located [here](https://github.com/Azure-Samples/appinsights-guestbook/blob/6555933e19d737b2ff4f9f339cc1b928f0c08cdb/client/src/AppContainer.js#L17-L20). Note that you must refresh the current operation's id **as well as** trigger an additional pageview:
**React Router history listener example**
```js
this.unlisten = this.props.history.listen((location, action) => {
ai.properties.context.telemetryTrace.traceID = Util.newId();
ai.trackPageView({name: window.location.pathname});
});
By default, this SDK will **not** handle state based route changing that occurs in single page applications unless you use a plugin designed for your frontend framework (Angular, React, Vue, etc). <!-- To enable enable automatic route change tracking for your single page application, you can add `enableAutoRouteTracking: true` to the setup configuration. -->
Currently, we support a separate [React plugin](#available-extensions-for-the-sdk) which you can initialize with this SDK. It will also accomplish route change tracking for you, as well as collect [other React specific telemetry](./vNext/extensions/applicationinsights-react-js).
```
## Examples

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

@ -1,6 +1,6 @@
/// <reference path="./TestFramework/Common.ts" />
import { Util, Exception, SeverityLevel, Trace, PageViewPerformance, PageView } from "@microsoft/applicationinsights-common";
import { Util, Exception, SeverityLevel, Trace, PageViewPerformance, PageView, IConfig } from "@microsoft/applicationinsights-common";
import {
ITelemetryItem, AppInsightsCore,
IPlugin, IConfiguration
@ -26,6 +26,88 @@ export class ApplicationInsightsTests extends TestClass {
}
public registerTests() {
this.testCase({
name: 'enableAutoRouteTracking: event listener is added to the popstate event',
test: () => {
// Setup
var appInsights = new ApplicationInsights();
var core = new AppInsightsCore();
var channel = new ChannelPlugin();
var eventListenerStub = this.sandbox.stub(window, 'addEventListener');
// Act
core.initialize(<IConfig & IConfiguration>{
instrumentationKey: '',
enableAutoRouteTracking: true
}, [appInsights, channel]);
// Assert
Assert.ok(eventListenerStub.calledTwice);
Assert.equal(eventListenerStub.args[0][0], "popstate");
Assert.equal(eventListenerStub.args[1][0], "locationchange");
}
});
this.testCase({
name: 'enableAutoRouteTracking: route changes trigger a new pageview',
test: () => {
// Setup
var appInsights = new ApplicationInsights();
var core = new AppInsightsCore();
var channel = new ChannelPlugin();
appInsights['_properties'] = <any>{
context: { telemetryTrace: { traceID: 'not set', name: 'name not set' } }
}
const trackPageViewStub = this.sandbox.stub(appInsights, 'trackPageView');
// Act
core.initialize(<IConfig & IConfiguration>{
instrumentationKey: '',
enableAutoRouteTracking: true
}, [appInsights, channel]);
window.dispatchEvent(new Event('locationchange'));
// Assert
Assert.ok(trackPageViewStub.calledOnce);
Assert.ok(appInsights['_properties'].context.telemetryTrace.traceID);
Assert.ok(appInsights['_properties'].context.telemetryTrace.name);
Assert.notEqual(appInsights['_properties'].context.telemetryTrace.traceID, 'not set', 'current operation id is updated after route change');
Assert.notEqual(appInsights['_properties'].context.telemetryTrace.name, 'name not set', 'current operation name is updated after route change');
}
});
this.testCase({
name: 'enableAutoRouteTracking: (IE9) app does not crash if history.pushState does not exist',
test: () => {
// Setup
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = null;
history.replaceState = null;
var appInsights = new ApplicationInsights();
var core = new AppInsightsCore();
var channel = new ChannelPlugin();
appInsights['_properties'] = <any>{
context: { telemetryTrace: { traceID: 'not set'}}
}
this.sandbox.stub(appInsights, 'trackPageView');
// Act
core.initialize(<IConfig & IConfiguration>{
instrumentationKey: '',
enableAutoRouteTracking: true
}, [appInsights, channel]);
window.dispatchEvent(new Event('locationchange'));
// Assert
Assert.ok(true, 'App does not crash when history object is incomplete');
// Cleanup
history.pushState = originalPushState;
history.replaceState = originalReplaceState;
}
});
this.testCase({
name: 'AppInsightsTests: PageVisitTimeManager is constructed when analytics plugin is initialized',
test: () => {

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

@ -17,6 +17,7 @@
"test": "grunt aitests"
},
"devDependencies": {
"@microsoft/applicationinsights-properties-js": "2.0.1",
"typescript": "2.5.3",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.1.0",

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

@ -4,12 +4,13 @@
*/
import {
IConfig, Util, PageViewPerformance, IAppInsights, PageView, RemoteDependencyData, Event, IEventTelemetry,
IConfig, Util, PageViewPerformance, IAppInsights, PageView, RemoteDependencyData, Event as EventTelemetry, IEventTelemetry,
TelemetryItemCreator, Metric, Exception, SeverityLevel, Trace, IDependencyTelemetry,
IExceptionTelemetry, ITraceTelemetry, IMetricTelemetry, IAutoExceptionTelemetry,
IPageViewTelemetryInternal, IPageViewTelemetry, IPageViewPerformanceTelemetry, IPageViewPerformanceTelemetryInternal,
ConfigurationManager, DateTimeUtils,
IExceptionInternal
IExceptionInternal,
PropertiesPluginIdentifier
} from "@microsoft/applicationinsights-common";
import {
@ -22,6 +23,9 @@ import { PageVisitTimeManager } from "./Telemetry/PageVisitTimeManager";
import { PageViewPerformanceManager } from './Telemetry/PageViewPerformanceManager';
import { ITelemetryConfig } from "../JavaScriptSDK.Interfaces/ITelemetryConfig";
// For types only
import * as properties from "@microsoft/applicationinsights-properties-js";
"use strict";
const durationProperty: string = "duration";
@ -39,6 +43,7 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
private _globalconfig: IConfiguration;
private _eventTracking: Timing;
private _pageTracking: Timing;
private _properties: properties.PropertiesPlugin;
protected _nextPlugin: ITelemetryPlugin;
protected _logger: IDiagnosticLogger; // Initialized by Core
protected _telemetryInitializers: { (envelope: ITelemetryItem): boolean | void; }[]; // Internal telemetry initializers.
@ -75,6 +80,8 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
config.isCookieUseDisabled = Util.stringToBoolOrDefault(config.isCookieUseDisabled);
config.isStorageUseDisabled = Util.stringToBoolOrDefault(config.isStorageUseDisabled);
config.isBrowserLinkTrackingEnabled = Util.stringToBoolOrDefault(config.isBrowserLinkTrackingEnabled);
config.enableAutoRouteTracking = Util.stringToBoolOrDefault(config.enableAutoRouteTracking);
config.namePrefix = config.namePrefix || "";
return config;
}
@ -113,8 +120,8 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
try {
let telemetryItem = TelemetryItemCreator.create<IEventTelemetry>(
event,
Event.dataType,
Event.envelopeType,
EventTelemetry.dataType,
EventTelemetry.envelopeType,
this._logger,
customProperties
);
@ -223,7 +230,7 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
public trackPageView(pageView?: IPageViewTelemetry, customProperties?: ICustomProperties) {
try {
const inPv = pageView || {};
this._pageViewManager.trackPageView(inPv, customProperties);
this._pageViewManager.trackPageView(inPv, {...inPv.properties, ...inPv.measurements, ...customProperties});
if (this.config.autoTrackPageVisitTime) {
this._pageVisitTimeManager.trackPreviousPageVisit(inPv.name, inPv.uri);
@ -540,12 +547,12 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
this.sendPageViewInternal(pageViewItem);
}
const instance: IAppInsights = this;
if (this.config.disableExceptionTracking === false &&
!this.config.autoExceptionInstrumented) {
// We want to enable exception auto collection and it has not been done so yet
const onerror = "onerror";
const originalOnError = window[onerror];
const instance: IAppInsights = this;
window.onerror = function (message, url, lineNumber, columnNumber, error) {
const handled = originalOnError && <any>originalOnError(message, url, lineNumber, columnNumber, error);
if (handled !== true) { // handled could be typeof function
@ -563,6 +570,47 @@ export class ApplicationInsights implements IAppInsights, ITelemetryPlugin, IApp
this.config.autoExceptionInstrumented = true;
}
/**
* Create a custom "locationchange" event which is triggered each time the history object is changed
*/
if (this.config.enableAutoRouteTracking === true
&& typeof history === "object" && typeof history.pushState === "function" && typeof history.replaceState === "function"
&& typeof window === "object") {
const _self = this;
// Find the properties plugin
extensions.forEach(extension => {
if (extension.identifier === PropertiesPluginIdentifier) {
this._properties = extension as properties.PropertiesPlugin;
}
});
history.pushState = ( f => function pushState() {
var ret = f.apply(this, arguments);
window.dispatchEvent(new Event(_self.config.namePrefix + "pushState"));
window.dispatchEvent(new Event(_self.config.namePrefix + "locationchange"));
return ret;
})(history.pushState);
history.replaceState = ( f => function replaceState(){
var ret = f.apply(this, arguments);
window.dispatchEvent(new Event(_self.config.namePrefix + "replaceState"));
window.dispatchEvent(new Event(_self.config.namePrefix + "locationchange"));
return ret;
})(history.replaceState);
window.addEventListener(_self.config.namePrefix + "popstate",()=>{
window.dispatchEvent(new Event(_self.config.namePrefix + "locationchange"));
});
window.addEventListener(_self.config.namePrefix + "locationchange", () => {
if (_self._properties && _self._properties.context && _self._properties.context.telemetryTrace) {
_self._properties.context.telemetryTrace.traceID = Util.newId();
_self._properties.context.telemetryTrace.name = window.location.pathname;
}
_self.trackPageView({ properties: { duration: 0 } }); // SPA route change loading durations are undefined, so send 0
});
}
this._isInitialized = true;
}

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

@ -94,6 +94,13 @@ export interface IConfig {
*/
autoTrackPageVisitTime?: boolean;
/**
* @description Automatically track route changes in Single Page Applications (SPA). If true, each route change will send a new Pageview to Application Insights.
* @type {boolean}
* @memberof IConfig
*/
enableAutoRouteTracking?: boolean;
/**
* @description If true, Ajax calls are not autocollected. Default is false
* @type {boolean}