Followup for [Beta][Task]15982357: Dynamic config for Properties extension #1972 (#1976)

- Add ability to tag Dynamic Config properties as referenced to support updating extensionConfig in-place with a new object instance
- Add IUnloadHookContainer to allow passing extension level unload hook container to sub-components
This commit is contained in:
Nev 2023-01-27 11:26:08 -08:00 коммит произвёл GitHub
Родитель 09dec1c433
Коммит af918812b6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 715 добавлений и 190 удалений

48
common/config/rush/npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -1753,9 +1753,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001448",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001448.tgz",
"integrity": "sha512-tq2YI+MJnooG96XpbTRYkBxLxklZPOdLmNIOdIhvf7SNJan6u5vCKum8iT7ZfCt70m1GPkuC7P3TtX6UuhupuA==",
"version": "1.0.30001449",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz",
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==",
"funding": [
{
"type": "opencollective",
@ -2225,9 +2225,9 @@
}
},
"node_modules/eslint-plugin-security": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.6.0.tgz",
"integrity": "sha512-SGvyejbhW/dziRbzOroKX5bj8z/qtBOw7Q95C9CBbJQqBtFB2o4OxSM3MCO2u9noPp7B6DDaFGtXTx8ImPiO/A==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.0.tgz",
"integrity": "sha512-+ahcCh7M5w7fdFaNccaChBGq8nd3Wa+XvGJS+hY74kvrMhG4EuLbljRIjilOqh1iDMW/EckB1oOWmiVIYlVACQ==",
"peer": true,
"dependencies": {
"safe-regex": "^2.1.1"
@ -3157,9 +3157,9 @@
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"node_modules/http-errors": {
"version": "2.0.0",
@ -5145,9 +5145,9 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@ -6856,9 +6856,9 @@
"peer": true
},
"caniuse-lite": {
"version": "1.0.30001448",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001448.tgz",
"integrity": "sha512-tq2YI+MJnooG96XpbTRYkBxLxklZPOdLmNIOdIhvf7SNJan6u5vCKum8iT7ZfCt70m1GPkuC7P3TtX6UuhupuA=="
"version": "1.0.30001449",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz",
"integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw=="
},
"chalk": {
"version": "4.1.2",
@ -7226,9 +7226,9 @@
}
},
"eslint-plugin-security": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.6.0.tgz",
"integrity": "sha512-SGvyejbhW/dziRbzOroKX5bj8z/qtBOw7Q95C9CBbJQqBtFB2o4OxSM3MCO2u9noPp7B6DDaFGtXTx8ImPiO/A==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.0.tgz",
"integrity": "sha512-+ahcCh7M5w7fdFaNccaChBGq8nd3Wa+XvGJS+hY74kvrMhG4EuLbljRIjilOqh1iDMW/EckB1oOWmiVIYlVACQ==",
"peer": true,
"requires": {
"safe-regex": "^2.1.1"
@ -7926,9 +7926,9 @@
"integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA=="
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"http-errors": {
"version": "2.0.0",
@ -9427,9 +9427,9 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"tsutils": {
"version": "3.21.0",

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

@ -1,6 +1,6 @@
import { Assert, AITestClass } from "@microsoft/ai-test-framework";
import { SinonStub } from 'sinon';
import { AppInsightsCore, DiagnosticLogger, createCookieMgr, newId, dateNow } from "@microsoft/applicationinsights-core-js";
import { AppInsightsCore, DiagnosticLogger, createCookieMgr, newId, dateNow, createDynamicConfig } from "@microsoft/applicationinsights-core-js";
import PropertiesPlugin from "../../../src/PropertiesPlugin";
import { _SessionManager } from "../../../src/Context/Session";
@ -64,13 +64,12 @@ export class SessionManagerTests extends AITestClass {
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: undefined,
sessionRenewalMs: undefined,
cookieDomain: undefined
};
}).cfg;
// Setup
let cookie = "";
const cookieStub: SinonStub = this.sandbox.stub(this.core.getCookieMgr(), 'set').callsFake((cookieName, value, maxAge, domain, path) => {
@ -92,14 +91,15 @@ export class SessionManagerTests extends AITestClass {
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionCookiePostfix: "testSessionCookieNamePostfix",
sessionExpirationMs: undefined,
sessionRenewalMs: undefined,
cookieDomain: undefined
};
}).cfg;
// Setup
let cookie = "";
const cookieStub: SinonStub = this.sandbox.stub(this.core.getCookieMgr(), 'set').callsFake((cookieName, value, maxAge, domain, path) => {
@ -121,12 +121,12 @@ export class SessionManagerTests extends AITestClass {
useFakeTimers: true,
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: 30 * 60 * 1000,
sessionRenewalMs: 24 * 60 * 60 * 1000,
cookieDomain: undefined
};
}).cfg;
// Simulate 100ms as when zero the cookie values are deemed to be invalid
this.clock.tick(100);
@ -152,12 +152,12 @@ export class SessionManagerTests extends AITestClass {
useFakeTimers: true,
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: 5000,
sessionRenewalMs: 24 * 60 * 60 * 1000,
cookieDomain: undefined
};
}).cfg;
// Simulate 100ms as when zero the cookie values are deemed to be invalid
this.clock.tick(100);
@ -211,12 +211,12 @@ export class SessionManagerTests extends AITestClass {
useFakeTimers: true,
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: 86400000,
sessionRenewalMs: 5000,
cookieDomain: undefined
};
}).cfg;
// Simulate 100ms as when zero the cookie values are deemed to be invalid
this.clock.tick(100);
@ -269,12 +269,12 @@ export class SessionManagerTests extends AITestClass {
useFakeTimers: true,
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: 30 * 60 * 1000,
sessionRenewalMs: 24 * 60 * 60 * 1000,
cookieDomain: undefined
};
}).cfg;
// Simulate 100ms as when zero the cookie values are deemed to be invalid
this.clock.tick(100);
@ -315,12 +315,12 @@ export class SessionManagerTests extends AITestClass {
useFakeTimers: true,
test: () => {
var sessionPrefix = newId();
var config = {
var config = createDynamicConfig({
namePrefix: sessionPrefix,
sessionExpirationMs: 5000,
sessionRenewalMs: 24 * 60 * 60 * 1000,
cookieDomain: undefined
};
}).cfg;
// Simulate 100ms as when zero the cookie values are deemed to be invalid
this.clock.tick(100);

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

@ -130,7 +130,6 @@ export class PropertiesTests extends AITestClass {
let extConfig = core.config.extensionConfig[id];
let exceptedDefaultConfig = {
instrumentationKey: "instrumentation_key",
accountId: null,
sessionRenewalMs: 30 * 60 * 1000,
samplingPercentage: 100,
@ -166,7 +165,6 @@ export class PropertiesTests extends AITestClass {
const channel = new TestChannelPlugin();
const properties = new PropertiesPlugin();
let exceptedDefaultConfig = {
instrumentationKey: "key1",
accountId: "id1",
sessionRenewalMs: 30 * 60 * 101,
samplingPercentage: 90,
@ -280,7 +278,6 @@ export class PropertiesTests extends AITestClass {
// change config
let newConfig = {
instrumentationKey: "key2",
accountId: "id2",
sessionRenewalMs: 30 * 60 * 102,
samplingPercentage: 90,
@ -375,8 +372,229 @@ export class PropertiesTests extends AITestClass {
core.getCookieMgr().del("ai_session" + "postfix2");
}
});
this.testCase({
name: "Properties Configuration: config can be set from root dynamically and updated as a new single object",
useFakeTimers: true,
test: () => {
const core = new AppInsightsCore();
core.setCookieMgr(createCookieMgr({
cookieCfg: {
setCookie: (name: string, value: string) => this._setCookie(name, value),
getCookie: (name: string) => this._getCookie(name),
delCookie: (name: string) => this._delCookie(name)
}
}, core.logger))
const channel = new TestChannelPlugin();
const properties = new PropertiesPlugin();
let exceptedDefaultConfig = {
accountId: "id1",
sessionRenewalMs: 30 * 60 * 101,
samplingPercentage: 90,
sessionExpirationMs: 24 * 60 * 60 * 101,
cookieDomain: "domain1",
sdkExtension: "ext1",
isBrowserLinkTrackingEnabled: false,
appId: "id1",
getSessionId: "session1",
namePrefix: "prefix1",
sessionCookiePostfix: "postfix1",
userCookiePostfix: "usercookie1",
idLength: 26,
getNewId: (idLength?: number) => {
return "" + (idLength || 0);
}
} as IPropertiesConfig;
let id = properties.identifier;
const config = {
instrumentationKey: "instrumentation_key",
extensionConfig: {
[id]: exceptedDefaultConfig
}
};
this.onDone(() => {
core.unload(false);
});
let appBuild = "build";
let deviceId = "newDeviceId";
let locId = "locId";
let parentId = "parentId";
let seId = "sessionId";
let cookie = "";
let cookieValue = ""
const cookieStub: SinonStub = this.sandbox.stub(core.getCookieMgr(), "set").callsFake((cookieName, value, maxAge, domain, path) => {
cookie = cookieName;
cookieValue = value;
});
// Initialize
core.initialize(config, [channel, properties]);
let extConfig = properties["_extConfig"];
Assert.deepEqual(extConfig, exceptedDefaultConfig, "intial config is set");
// check inital context
let propCtx = properties.context;
let appId = propCtx.appId();
Assert.equal(appId, null, "appId should be null");
let application = propCtx.application;
Assert.equal(application.build, null, "application build should be null by default");
let device = propCtx.device;
Assert.equal(device.id, "browser", "device id should not be null");
let location = propCtx.location;
Assert.ok(location, "location should not be null");
let trace = propCtx.telemetryTrace;
let traceId = trace.traceID;
Assert.ok(trace, "trace should not be null");
Assert.ok(trace.name, "trace name should not be null");
Assert.ok(traceId, "trace id should not be null");
Assert.ok(!trace.parentID, "trace parent should be null");
let user = propCtx.user;
Assert.deepEqual(user.config, exceptedDefaultConfig, "user config should be updated");
let internalSdkVer = propCtx.internal.sdkVersion;
Assert.ok(internalSdkVer.indexOf("ext1") > -1, "sdk ext prefix should be used");
let session = propCtx.session;
Assert.ok(session, "session should not be null");
let sessionMgr = propCtx.sessionManager;
Assert.ok(sessionMgr.automaticSession, "session mgr should not be null");
let os = propCtx.os;
Assert.equal(os, null, "os should be null");
let web = propCtx.web;
Assert.equal(web, null, "web should be null");
let sessionId = propCtx.getSessionId();
Assert.equal(sessionId, null, "session Id should be null");
// change properities here to make sure we won't overwrite them after config change
device.id = deviceId;
location.ip = locId;
trace.parentID = parentId;
application.build = appBuild;
session.id = seId;
sessionMgr.automaticSession.id = seId;
Assert.equal(propCtx.getSessionId(), seId, "session Id should be updated test1");
let sessionPrefix = "ai_session" + "postfix1";
sessionMgr.backup();
let sessionStorage = utlGetLocalStorage(core.logger, sessionPrefix);
Assert.ok(sessionStorage.indexOf(seId) > -1, "sessionStorage should be set based on session id test1");
sessionMgr.update();
Assert.ok(cookieStub.called, "cookie set test1");
Assert.equal(sessionPrefix, cookie, "cookie name is set test1");
Assert.ok(cookieValue.indexOf(seId) > -1, "cookie value is set test1");
this.clock.tick(20);
sessionMgr.update();
Assert.equal(sessionMgr.automaticSession.id, seId, "session id should be same test1");
this.clock.tick(24 * 60 * 60 * 101 - 20);
sessionMgr.update();
Assert.equal(sessionMgr.automaticSession.id, "26", "session id should be renewed test1");
Assert.ok(cookieValue.indexOf("26|") > -1,"cookie value is reset test1");
// change config
let newConfig = {
accountId: "id2",
sessionRenewalMs: 30 * 60 * 102,
samplingPercentage: 90,
sessionExpirationMs: 24 * 60 * 60 * 102,
cookieDomain: "domain2",
sdkExtension: "ext2",
isBrowserLinkTrackingEnabled: true,
appId: "id2",
getSessionId: "session2",
namePrefix: "prefix2",
sessionCookiePostfix: "postfix2",
userCookiePostfix: "usercookie2",
idLength: 26,
getNewId: (idLength?: number) => {
return "" + (idLength || 0) + 1;
}
} as IPropertiesConfig;
core.config.extensionConfig = core.config.extensionConfig? core.config.extensionConfig : {};
core.config.extensionConfig[id] = newConfig;
this.clock.tick(1);
// properties that should be updated
extConfig = properties["_extConfig"];
Assert.deepEqual(extConfig, newConfig, "extConfig should be updated");
propCtx = properties.context;
user = propCtx.user;
Assert.deepEqual(user.config, newConfig, "user config should be updated");
internalSdkVer = propCtx.internal.sdkVersion;
Assert.ok(internalSdkVer.indexOf("ext2") > -1, "sdk ext prefix should be used and updated");
sessionMgr = propCtx.sessionManager;
Assert.ok(sessionMgr.automaticSession, "session mgr should not be null");
sessionPrefix = "ai_session" + "postfix2";
sessionMgr.backup();
sessionStorage = utlGetLocalStorage(core.logger, sessionPrefix);
Assert.ok(sessionStorage.indexOf("26") > -1, "sessionStorage should be updated based on session id test2");
Assert.ok(cookieValue.indexOf("26|") > -1, "cookie should not be updated test2");
Assert.equal(sessionMgr.automaticSession.id, "26", "session id should not be renewed test2");
this.clock.tick(60000);
sessionMgr.update();
Assert.ok(cookieStub.called, "cookie set test3");
Assert.equal(sessionPrefix, cookie, "cookie name is set test3");
Assert.equal(sessionMgr.automaticSession.id, "26", "session id should not be renewed test3");
Assert.ok(cookieValue.indexOf("26|") > -1, "cookie value should use previous session id test3");
Assert.equal(sessionMgr.automaticSession.id, "26", "session id should not be renewed test4");
this.clock.tick(24 * 60 * 60 * 102);
sessionMgr.update();
Assert.ok(cookieStub.called, "cookie set test5");
Assert.equal(sessionPrefix, cookie, "cookie name is set test5");
Assert.ok(cookieValue.indexOf("261|") > -1, "cookie value is renewed test6");
Assert.equal(sessionMgr.automaticSession.id, "261", "session id should be renewed test6");
//properties that should not be updated
appId = propCtx.appId();
Assert.equal(appId, null, "appId should be null");
application = propCtx.application;
Assert.equal(application.build, appBuild, "application build should not be updated");
device = propCtx.device;
Assert.equal(device.id, deviceId, "device id should not be updated");
location = propCtx.location;
Assert.equal(location.ip, locId, "location should not be updated");
trace = propCtx.telemetryTrace;
Assert.ok(trace, "trace should not be null");
Assert.ok(trace.name, "trace name should not be null");
Assert.equal(trace.traceID, traceId, "trace id should be same with previous one");
Assert.equal(trace.parentID, parentId, "trace parent should not be updated");
session = propCtx.session;
Assert.equal(session.id, seId, "session should not be updated");
os = propCtx.os;
Assert.equal(os, null,"os should not be updated");
sessionId = propCtx.getSessionId();
Assert.equal(sessionId, seId, "session Id should not be updated");
if (utlCanUseLocalStorage()) {
window.localStorage.clear();
}
core.getCookieMgr().del("ai_session" + "postfix1");
core.getCookieMgr().del("ai_session" + "postfix2");
}
});
}
private addDeviceTests() {
this.testCase({
name: 'Device: device context adds Browser field to ITelemetryItem',
@ -962,7 +1180,6 @@ export class PropertiesTests extends AITestClass {
private getTelemetryConfig(): IPropertiesConfig {
return {
instrumentationKey: "",
accountId: "",
sessionRenewalMs: 1000,
samplingPercentage: 0,

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

@ -2,7 +2,7 @@
// Licensed under the MIT License.
import { IInternal } from "@microsoft/applicationinsights-common";
import { onConfigChange } from "@microsoft/applicationinsights-core-js";
import { IUnloadHookContainer, onConfigChange } from "@microsoft/applicationinsights-core-js";
import { IPropertiesConfig } from "../Interfaces/IPropertiesConfig";
const Version = "2.8.5";
@ -37,11 +37,13 @@ export class Internal implements IInternal {
/**
* Constructs a new instance of the internal telemetry data class.
*/
constructor(config: IPropertiesConfig) {
constructor(config: IPropertiesConfig, unloadHookContainer?: IUnloadHookContainer) {
onConfigChange((config), () => {
let unloadHook = onConfigChange((config), () => {
let prefix = config.sdkExtension;
this.sdkVersion = (prefix ? prefix + "_" : "") + "javascript:" + Version;
});
unloadHookContainer && unloadHookContainer.add(unloadHook);
}
}

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

@ -4,8 +4,8 @@
import dynamicProto from "@microsoft/dynamicproto-js";
import { ISession, utlCanUseLocalStorage, utlGetLocalStorage, utlSetLocalStorage } from "@microsoft/applicationinsights-common";
import {
IAppInsightsCore, ICookieMgr, IDiagnosticLogger, _eInternalMessageId, _throwInternal, dateNow, dumpObj, eLoggingSeverity,
getExceptionName, isFunction, newId, safeGetCookieMgr, safeGetLogger
IAppInsightsCore, ICookieMgr, IDiagnosticLogger, IUnloadHookContainer, _eInternalMessageId, _throwInternal, dateNow, dumpObj,
eLoggingSeverity, getExceptionName, isFunction, newId, onConfigChange, safeGetCookieMgr, safeGetLogger
} from "@microsoft/applicationinsights-core-js";
const SESSION_COOKIE_NAME = "ai_session";
@ -51,11 +51,14 @@ export class _SessionManager {
public automaticSession: Session;
constructor(config: ISessionConfig, core?: IAppInsightsCore) {
constructor(config: ISessionConfig, core?: IAppInsightsCore, unloadHookContainer?: IUnloadHookContainer) {
let self = this;
let _storageNamePrefix: string;
let _cookieUpdatedTimestamp: number;
let _logger: IDiagnosticLogger = safeGetLogger(core);
let _cookieManager: ICookieMgr = safeGetCookieMgr(core);
let _sessionExpirationMs: number;
let _sessionRenewalMs: number;
dynamicProto(_SessionManager, self, (_self) => {
@ -63,6 +66,18 @@ export class _SessionManager {
config = ({} as any);
}
let unloadHook = onConfigChange(config, (details) => {
_sessionExpirationMs = config.sessionExpirationMs || ACQUISITION_SPAN;
_sessionRenewalMs = config.sessionRenewalMs || RENEWAL_SPAN;
// sessionCookiePostfix takes the preference if it is configured, otherwise takes namePrefix if configured.
const sessionCookiePostfix = config.sessionCookiePostfix || config.namePrefix || "";
_storageNamePrefix = SESSION_COOKIE_NAME + sessionCookiePostfix;
});
unloadHookContainer && unloadHookContainer.add(unloadHook);
_self.automaticSession = new Session();
_self.update = () => {
@ -76,12 +91,12 @@ export class _SessionManager {
isExpired = !_initializeAutomaticSession(session, nowMs);
}
if (!isExpired && _sessionExpirationMs() > 0) {
if (!isExpired && _sessionExpirationMs > 0) {
const timeSinceAcqMs = nowMs - session.acquisitionDate;
const timeSinceRenewalMs = nowMs - session.renewalDate;
isExpired = timeSinceAcqMs < 0 || timeSinceRenewalMs < 0; // expired if the acquisition or last renewal are in the future
isExpired = isExpired || timeSinceAcqMs > _sessionExpirationMs(); // expired if the time since acquisition is more than session Expiration
isExpired = isExpired || timeSinceRenewalMs > _sessionRenewalMs(); // expired if the time since last renewal is more than renewal period
isExpired = isExpired || timeSinceAcqMs > _sessionExpirationMs; // expired if the time since acquisition is more than session Expiration
isExpired = isExpired || timeSinceRenewalMs > _sessionRenewalMs; // expired if the time since last renewal is more than renewal period
}
// renew if acquisitionSpan or renewalSpan has elapsed
@ -106,21 +121,6 @@ export class _SessionManager {
_setStorage(session.id, session.acquisitionDate, session.renewalDate);
};
//TODO: move _storageNamePrefix, _sessionExpirationMs _sessionRenewalMs back to local variables after dynamic config fix
function _storageNamePrefix(): string {
// sessionCookiePostfix takes the preference if it is configured, otherwise takes namePrefix if configured.
let sessionCookiePostfix = config.sessionCookiePostfix || config.namePrefix || "";
return SESSION_COOKIE_NAME + sessionCookiePostfix;
}
function _sessionExpirationMs(): number {
return config.sessionExpirationMs || ACQUISITION_SPAN;
}
function _sessionRenewalMs(): number {
return config.sessionRenewalMs || RENEWAL_SPAN;
}
/**
* Use config.namePrefix + ai_session cookie data or local storage data (when the cookie is unavailable) to
* initialize the automatic session.
@ -128,7 +128,7 @@ export class _SessionManager {
*/
function _initializeAutomaticSession(session: ISession, now: number): boolean {
let isValid = false;
const cookieValue = _cookieManager.get(_storageNamePrefix());
const cookieValue = _cookieManager.get(_storageNamePrefix);
if (cookieValue && isFunction(cookieValue.split)) {
isValid = _initializeAutomaticSessionWithData(session, cookieValue);
} else {
@ -136,7 +136,7 @@ export class _SessionManager {
// This can happen if the session expired or the user actively deleted the cookie
// We only want to recover data if the cookie is missing from expiry. We should respect the user's wishes if the cookie was deleted actively.
// The User class handles this for us and deletes our local storage object if the persistent user cookie was removed.
const storageValue = utlGetLocalStorage(_logger, _storageNamePrefix());
const storageValue = utlGetLocalStorage(_logger, _storageNamePrefix);
if (storageValue) {
isValid = _initializeAutomaticSessionWithData(session, storageValue);
}
@ -210,11 +210,11 @@ export class _SessionManager {
let acq = session.acquisitionDate;
session.renewalDate = nowMs;
let renewalPeriodMs = _sessionRenewalMs();
let renewalPeriodMs = _sessionRenewalMs;
// Set cookie to expire after the session expiry time passes or the session renewal deadline, whichever is sooner
// Expiring the cookie will cause the session to expire even if the user isn't on the page
const acqTimeLeftMs = (acq +_sessionExpirationMs()) - nowMs;
const acqTimeLeftMs = (acq +_sessionExpirationMs) - nowMs;
const cookie = [session.id, acq, nowMs];
let maxAgeSec = 0;
@ -229,7 +229,7 @@ export class _SessionManager {
// if sessionExpirationMs is set to 0, it means the expiry is set to 0 for this session cookie
// A cookie with 0 expiry in the session cookie will never expire for that browser session. If the browser is closed the cookie expires.
// Depending on the browser, another instance does not inherit this cookie, however, another tab will
_cookieManager.set(_storageNamePrefix(), cookie.join("|"), _sessionExpirationMs() > 0 ? maxAgeSec : null, cookieDomain);
_cookieManager.set(_storageNamePrefix, cookie.join("|"), _sessionExpirationMs > 0 ? maxAgeSec : null, cookieDomain);
_cookieUpdatedTimestamp = nowMs;
}
@ -237,7 +237,7 @@ export class _SessionManager {
// Keep data in local storage to retain the last session id, allowing us to cleanly end the session when it expires
// Browsers that don't support local storage won't be able to end sessions cleanly from the client
// The server will notice this and end the sessions itself, with loss of accurate session duration
utlSetLocalStorage(_logger, _storageNamePrefix(), [guid, acq, renewal].join("|"));
utlSetLocalStorage(_logger, _storageNamePrefix, [guid, acq, renewal].join("|"));
}
});
}

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

@ -4,8 +4,8 @@
import dynamicProto from "@microsoft/dynamicproto-js";
import { IUserContext, utlRemoveStorage } from "@microsoft/applicationinsights-common";
import {
IAppInsightsCore, ICookieMgr, _eInternalMessageId, _throwInternal, eLoggingSeverity, newId, onConfigChange, safeGetCookieMgr,
safeGetLogger, toISOString
IAppInsightsCore, ICookieMgr, IUnloadHookContainer, _eInternalMessageId, _throwInternal, eLoggingSeverity, newId, onConfigChange,
safeGetCookieMgr, safeGetLogger, toISOString
} from "@microsoft/applicationinsights-core-js";
import { objDefineProp } from "@nevware21/ts-utils";
import { IPropertiesConfig } from "../Interfaces/IPropertiesConfig";
@ -69,7 +69,7 @@ export class User implements IUserContext {
*/
public isUserCookieSet = false;
constructor(config: IPropertiesConfig, core: IAppInsightsCore) {
constructor(config: IPropertiesConfig, core: IAppInsightsCore, unloadHookContainer?: IUnloadHookContainer) {
let _logger = safeGetLogger(core);
let _cookieManager: ICookieMgr = safeGetCookieMgr(core);
let _storageNamePrefix: string;
@ -82,7 +82,7 @@ export class User implements IUserContext {
get: () => config
});
onConfigChange(config, () => {
let unloadHook = onConfigChange(config, () => {
const userCookiePostfix = config.userCookiePostfix || "";
_storageNamePrefix = User.userCookieName + userCookiePostfix;
@ -131,6 +131,8 @@ export class User implements IUserContext {
}
});
unloadHookContainer && unloadHookContainer.add(unloadHook);
function _generateNewId() {
let theConfig = (config || {}) as IPropertiesConfig;
let getNewId = theConfig.getNewId || newId;

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

@ -2,7 +2,6 @@
// Licensed under the MIT License.
export interface IPropertiesConfig {
readonly instrumentationKey: string;
readonly accountId: string;
readonly sessionRenewalMs: number;
readonly samplingPercentage: number;

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

@ -21,7 +21,6 @@ let undefString: string;
const nullValue: any = null;
const _defaultConfig: IConfigDefaults<IPropertiesConfig> = objDeepFreeze({
instrumentationKey: undefString,
accountId: nullValue,
sessionRenewalMs: 30 * 60 * 1000,
samplingPercentage: 100,
@ -68,14 +67,6 @@ export default class PropertiesPlugin extends BaseTelemetryPlugin implements IPr
_self.initialize = (config: IConfiguration & IConfig, core: IAppInsightsCore, extensions: IPlugin[], pluginChain?:ITelemetryPluginChain) => {
_base.initialize(config, core, extensions, pluginChain);
_populateDefaults(config);
_previousTraceCtx = core.getTraceCtx(false);
_context = new TelemetryContext(core, _extensionConfig, _previousTraceCtx);
_distributedTraceCtx = createDistributedTraceContextFromTrace(_self.context.telemetryTrace, _previousTraceCtx);
core.setTraceCtx(_distributedTraceCtx);
_self.context.appId = () => {
let breezeChannel = core.getPlugin<IPlugin>(BreezeChannelIdentifier);
return breezeChannel ? breezeChannel.plugin["_appId"] : null;
};
};
/**
@ -147,6 +138,16 @@ export default class PropertiesPlugin extends BaseTelemetryPlugin implements IPr
// Test hook to allow accessing the internal values -- explicitly not defined as an available property on the class
_self["_extConfig"] = _extensionConfig;
}));
// This is outside of the onConfigChange as we don't want to update (replace) these values whenever a referenced config item changes
_previousTraceCtx = core.getTraceCtx(false);
_context = new TelemetryContext(core, _extensionConfig, _previousTraceCtx, _self._unloadHooks);
_distributedTraceCtx = createDistributedTraceContextFromTrace(_self.context.telemetryTrace, _previousTraceCtx);
core.setTraceCtx(_distributedTraceCtx);
_self.context.appId = () => {
let breezeChannel = core.getPlugin<IPlugin>(BreezeChannelIdentifier);
return breezeChannel ? breezeChannel.plugin["_appId"] : null;
};
}
function _processTelemetryInternal(evt: ITelemetryItem, itemCtx: IProcessTelemetryContext) {

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

@ -9,8 +9,8 @@ import {
PageView
} from "@microsoft/applicationinsights-common";
import {
IAppInsightsCore, IDistributedTraceContext, IProcessTelemetryContext, ITelemetryItem, _InternalLogMessage, getSetValue, hasWindow,
isNullOrUndefined, isString, objKeys, setValue
IAppInsightsCore, IDistributedTraceContext, IProcessTelemetryContext, ITelemetryItem, IUnloadHookContainer, _InternalLogMessage,
getSetValue, hasWindow, isNullOrUndefined, isString, objKeys, setValue
} from "@microsoft/applicationinsights-core-js";
import { Application } from "./Context/Application";
import { Device } from "./Context/Device";
@ -50,19 +50,19 @@ export class TelemetryContext implements IPropTelemetryContext {
public appId: () => string;
public getSessionId: () => string;
constructor(core: IAppInsightsCore, defaultConfig: IPropertiesConfig, previousTraceCtx?: IDistributedTraceContext) {
constructor(core: IAppInsightsCore, defaultConfig: IPropertiesConfig, previousTraceCtx?: IDistributedTraceContext, unloadHookContainer?: IUnloadHookContainer) {
let logger = core.logger
dynamicProto(TelemetryContext, this, (_self) => {
_self.appId = _nullResult;
_self.getSessionId = _nullResult;
_self.application = new Application();
_self.internal = new Internal(defaultConfig);
_self.internal = new Internal(defaultConfig, unloadHookContainer);
if (hasWindow()) {
_self.sessionManager = new _SessionManager(defaultConfig, core);
_self.sessionManager = new _SessionManager(defaultConfig, core, unloadHookContainer);
_self.device = new Device();
_self.location = new Location();
_self.user = new User(defaultConfig, core);
_self.user = new User(defaultConfig, core, unloadHookContainer);
let traceId: string;
let parentId: string;

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

@ -5,7 +5,7 @@ import { IConfigDefaults } from "../../../src/Config/IConfigDefaults";
import { IConfiguration } from "../../../src/JavaScriptSDK.Interfaces/IConfiguration";
import { getDynamicConfigHandler } from "../../../src/Config/DynamicSupport";
import { createDynamicConfig, onConfigChange } from "../../../src/Config/DynamicConfig";
import { arrForEach, dumpObj, isArray, isFunction, objForEachKey, objKeys, isPlainObject } from "@nevware21/ts-utils";
import { arrForEach, dumpObj, isArray, isFunction, objForEachKey, objKeys, isPlainObject, objHasOwn } from "@nevware21/ts-utils";
import { IAppInsightsCore } from "../../../src/JavaScriptSDK.Interfaces/IAppInsightsCore";
import { INotificationManager } from "../../../src/JavaScriptSDK.Interfaces/INotificationManager";
import { IPerfManager } from "../../../src/JavaScriptSDK.Interfaces/IPerfManager";
@ -23,6 +23,14 @@ const verifyArray = <T>() => <U extends NoRepeats<U> & readonly T[]>(
u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;
function _expectException(cb: () => void, message: string) {
try {
cb();
QUnit.ok(false, "Expected an exception: " + (message || ""));
} catch (e) {
QUnit.ok(true, message);
}
}
export class DynamicConfigTests extends AITestClass {
public testInitialize() {
@ -553,6 +561,196 @@ export class DynamicConfigTests extends AITestClass {
}
});
this.testCase({
name: "Validate updating objects / arrays replaces the contents in-place",
useFakeTimers: true,
test: () => {
let theConfig: any = {
instrumentationKey: "testiKey",
endpointUrl: "https://localhost:9001",
enableDebugExceptions: false,
loggingLevelConsole: 1,
extensionConfig: {
"test": {} as any
},
userCfg: {
userTest: {} as any
}
};
const channelPlugin = new TestChannelPlugin();
let core = new AppInsightsCore();
core.initialize(theConfig, [channelPlugin]);
let cfg = core.config as any;
// Grab a copy of the testExtCfg
let testExtCfg = cfg.extensionConfig?.test;
let userTestCfg = cfg.userCfg.userTest;
let helloCfg: any = {
"hello": "World"
};
let myCfg: any = {
"my": "newCfg"
};
Assert.ok(!objHasOwn(cfg.extensionConfig!.test, "hello"), "The hello property should not exist");
Assert.ok(!objHasOwn(testExtCfg, "hello"), "The hello property should not exist");
// Assign the new config
cfg.extensionConfig!.test = helloCfg;
// Assign the new config to a new key
cfg.extensionConfig!.newTest = helloCfg;
// The test should now have "copied" the helloCfg
Assert.ok(objHasOwn(cfg.extensionConfig!.test, "hello"), "The hello property should exist");
Assert.equal("World", cfg.extensionConfig!.test.hello, "And should be assigned");
Assert.ok(cfg.extensionConfig!.test !== helloCfg, "The new config should have copied but not directly referenced the helloCfg");
// Check that the previously referenced config also has the value
Assert.ok(objHasOwn(testExtCfg, "hello"), "The hello property should exist");
Assert.equal("World", testExtCfg.hello, "And should be assigned");
Assert.ok(cfg.extensionConfig!.test === testExtCfg, "The previous reference and the current value should still be the same instance");
// The newTest element should refer to the helloCfg as it was NOT tagged as referenced
Assert.ok(objHasOwn(cfg.extensionConfig!.newTest, "hello"), "The hello property should exist");
Assert.equal("World", cfg.extensionConfig!.newTest.hello, "And should be assigned");
Assert.ok(cfg.extensionConfig!.newTest === helloCfg, "The new config should have directly referenced the helloCfg as it was not previously present or referenced");
// Final validation that test !== newTest
Assert.ok(cfg.extensionConfig!.newTest !== cfg.extensionConfig!.test, "NewTest and old test should not reference the same instance");
// Assign the new config
cfg.extensionConfig!.test = myCfg;
// The test should now have "copied" the helloCfg
Assert.ok(objHasOwn(cfg.extensionConfig!.test, "hello"), "The hello property should still exist");
Assert.equal(undefined, cfg.extensionConfig!.test.hello, "But hello should be undefined");
Assert.ok(objHasOwn(cfg.extensionConfig!.test, "my"), "But the my property should");
Assert.equal("newCfg", cfg.extensionConfig!.test.my, "And should be assigned");
Assert.ok(cfg.extensionConfig!.test !== myCfg, "The new config should have copied but not directly referenced the helloCfg");
// Check that the previously referenced config also has the value
Assert.ok(objHasOwn(testExtCfg, "hello"), "The hello property should exist");
Assert.equal(undefined, cfg.extensionConfig!.test.hello, "But hello should be undefined");
Assert.ok(objHasOwn(testExtCfg, "my"), "The my property should exist");
Assert.equal("newCfg", testExtCfg.my, "And should be assigned");
Assert.ok(cfg.extensionConfig!.test === testExtCfg, "The previous reference and the current value should still be the same instance");
// New Test should be unchanged
// The newTest element should refer to the helloCfg as it was NOT tagged as referenced
Assert.ok(objHasOwn(cfg.extensionConfig!.newTest, "hello"), "The hello property should exist");
Assert.equal("World", cfg.extensionConfig!.newTest.hello, "And should be assigned");
Assert.ok(cfg.extensionConfig!.newTest === helloCfg, "The new config should have directly referenced the helloCfg as it was not previously present or referenced");
// Final validation that test !== newTest
Assert.ok(cfg.extensionConfig!.newTest !== cfg.extensionConfig!.test, "NewTest and old test should not reference the same instance");
// ---------------------------------------------------------
// Validate that updating non-referenced objects replaces the object
Assert.ok(!objHasOwn(cfg.userCfg.userTest, "hello"), "The hello property should not exist");
Assert.ok(!objHasOwn(userTestCfg, "hello"), "The hello property should not exist");
// Assign the new config
cfg.userCfg!.userTest = helloCfg;
// Assign the new config to a new key
cfg.userCfg!.newTest = helloCfg;
// The test should now have "copied" the helloCfg
Assert.ok(objHasOwn(cfg.userCfg!.userTest, "hello"), "The hello property should exist");
Assert.equal("World", cfg.userCfg!.userTest.hello, "And should be assigned");
Assert.ok(cfg.userCfg!.userTest === helloCfg, "The new config should directly referenced the helloCfg");
// The original reference obtained before updating the userCfg should not have changed
Assert.ok(!objHasOwn(userTestCfg, "hello"), "The hello property should not exist");
// The newTest element should refer to the helloCfg
Assert.ok(objHasOwn(cfg.userCfg!.newTest, "hello"), "The hello property should exist");
Assert.equal("World", cfg.userCfg!.newTest.hello, "And should be assigned");
Assert.ok(cfg.userCfg!.newTest === helloCfg, "The new config should have directly referenced the helloCfg");
// Final validation that test !== newTest
Assert.ok(cfg.userCfg!.newTest === cfg.userCfg!.userTest, "NewTest and old test should reference the same instance");
// Assign the new config
cfg.userCfg!.userTest = myCfg;
// The test should now have "copied" the helloCfg
Assert.ok(!objHasOwn(cfg.userCfg!.userTest, "hello"), "The hello property should not exist");
Assert.ok(objHasOwn(cfg.userCfg!.userTest, "my"), "The my property should exist");
Assert.equal("newCfg", cfg.userCfg!.userTest.my, "And should be assigned");
Assert.ok(cfg.userCfg!.userTest === myCfg, "The new config should directly referenced the myCfg");
// The original reference obtained before updating the userCfg should not have changed
Assert.ok(!objHasOwn(userTestCfg, "hello"), "The hello property should not exist");
Assert.ok(!objHasOwn(userTestCfg, "my"), "The my property should not exist");
// The newTest element should still reference the helloCfg
Assert.ok(objHasOwn(cfg.userCfg!.newTest, "hello"), "The hello property should exist");
Assert.equal("World", cfg.userCfg!.newTest.hello, "And should be assigned");
Assert.ok(cfg.userCfg!.newTest === helloCfg, "The new config should have directly referenced the helloCfg");
// Final validation that test !== newTest
Assert.ok(cfg.userCfg!.newTest !== cfg.userCfg!.test, "NewTest and old test should not reference the same instance");
}
});
this.testCase({
name: "Validate read-only",
useFakeTimers: true,
test: () => {
let theConfig: any = {
instrumentationKey: "testiKey",
endpointUrl: "https://localhost:9001",
enableDebugExceptions: false,
loggingLevelConsole: 1,
extensionConfig: {
"test": {} as any
},
anArray: [ 1, 2, 3 ]
};
let handler = createDynamicConfig(theConfig, undefined, undefined, true);
handler.rdOnly(theConfig, "instrumentationKey");
_expectException(() => {
theConfig.instrumentationKey = "newTestKey";
}, "Should not be able to re-assign instrumentationKey");
// Re-Assigning with the same value doesn't cause an exception
theConfig.instrumentationKey = "testiKey";
handler.rdOnly(theConfig, "extensionConfig");
_expectException(() => {
theConfig.extensionConfig = {};
}, "Should not be able to re-assign extensionConfig");
// Assigning a property to a read-only property is allowed
theConfig.extensionConfig.hello = "World";
QUnit.assert.equal("World", theConfig.extensionConfig.hello, "Hello should be assigned")
QUnit.assert.deepEqual({}, theConfig.extensionConfig.test, "test should be assigned")
handler.rdOnly(theConfig, "anArray");
_expectException(() => {
theConfig.anArray = [];
}, "Should not be able to re-assign anArray");
// Assigning a property to a read-only property is allowed
theConfig.anArray.hello = "World";
QUnit.assert.equal("World", theConfig.anArray.hello, "Hello should be assigned")
theConfig.anArray[0] = 0;
QUnit.assert.equal(0, theConfig.anArray[0], "0");
QUnit.assert.equal(2, theConfig.anArray[1], "2");
QUnit.assert.equal(3, theConfig.anArray[2], "3");
}
});
function createPerfMgr(core: IAppInsightsCore, manager: INotificationManager): IPerfManager {
return createPerfMgr(core, manager);
}

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

@ -73,6 +73,16 @@ function _createDynamicHandler<T extends IConfiguration>(logger: IDiagnosticLogg
theState.use(null, configHandler);
}
function _ref<C>(target: C, name: string) {
// Make sure it's dynamic and mark as referenced with it's current value
return _setDynamicProperty(theState, target, name, target[name], true);
}
function _rdOnly<C>(target: C, name: string) {
// Make sure it's dynamic and mark as readonly with it's current value
return _setDynamicProperty(theState, target, name, target[name], false, true);
}
function _applyDefaults<C>(theConfig: C, defaultValues: IConfigDefaults<C, T>): C {
if (defaultValues) {
// Resolve/apply the defaults
@ -93,6 +103,8 @@ function _createDynamicHandler<T extends IConfiguration>(logger: IDiagnosticLogg
set: _setValue,
setDf: _applyDefaults,
watch: _watch,
ref: _ref,
rdOnly: _rdOnly,
_block: _block
};

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

@ -33,7 +33,12 @@ function _patchArray<T>(state: _IDynamicConfigHandlerState<T>, target: any) {
}
}
function _makeDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<T>, theConfig: C, name: string, value: V) {
function _getOwnPropGetter<T>(target: T, name: PropertyKey) {
let propDesc = objGetOwnPropertyDescriptor(target, name);
return propDesc && propDesc.get;
}
function _makeDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<T>, theConfig: C, name: string, value: V): V {
// Does not appear to be dynamic so lets make it so
let detail: _IDynamicDetail<T> = {
n: name,
@ -88,23 +93,36 @@ function _makeDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<
function _setProperty(newValue: V) {
if (value !== newValue) {
if (!!_setProperty[state.ro] && state.upd) {
if (!!_getProperty[state.ro] && !state.upd) {
// field is marked as readonly so return false
throwInvalidAccess("[" + name + "] is sealed from " + dumpObj(theConfig));
throwInvalidAccess("[" + name + "] is read-only:" + dumpObj(theConfig));
}
// As we are replacing the value, if already dynamic then we need to notify any listeners that changes
// are happening
if (value && value[CFG_HANDLER_LINK]) {
// For objects / arrays, we can indirectly inform any listeners by just changing the value to undefined
// This will trigger any listeners by simply calling their version of the setter.
if (isPlainObject(value) || isArray(value)) {
let isReferenced = _getProperty[state.rf];
if(isPlainObject(value) || isArray(value)) {
if (isReferenced) {
// Reassign the properties from the current value to the same properties from the newValue
// This will set properties not in the newValue to undefined
objForEachKey(value, (key) => {
value[key] = newValue[key];
});
// Now assign / re-assign value with all of the keys from newValue
objForEachKey(newValue, (key, theValue) => {
_setDynamicProperty(state, value, key, theValue);
});
// Now drop newValue so when we assign value later it keeps the existing reference
newValue = value;
} else if (value && value[CFG_HANDLER_LINK]) {
// As we are replacing the value, if it's already dynamic then we need to notify the listeners
// for every property it has already
objForEachKey(value, (key) => {
// Check if the value is dynamic
let propDesc = objGetOwnPropertyDescriptor(value, key);
if (propDesc && propDesc.get) {
let getter = _getOwnPropGetter(value, key);
if (getter) {
// And if it is tell it's listeners that the value has changed
let valueState: _IDynamicGetter = propDesc.get[state.prop];
let valueState: _IDynamicGetter = getter[state.prop];
valueState && valueState.chng();
}
});
@ -112,10 +130,12 @@ function _makeDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<
}
checkDynamic = false;
if (isPlainObject(newValue) || isArray(newValue)) {
if (!isReferenced && (isPlainObject(newValue) || isArray(newValue))) {
// As the newValue is an object/array lets preemptively make it dynamic
_makeDynamicObject(state, newValue);
}
// Now assign the internal "value" to the newValue
value = newValue;
// Cause any listeners to be scheduled for notification
@ -124,23 +144,35 @@ function _makeDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<
}
objDefineAccessors(theConfig, detail.n, _getProperty, _setProperty, true);
// Return the dynamic reference
return _getProperty();
}
export function _setDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<T>, target: C, name: string, value: V) {
export function _setDynamicProperty<T, C, V = any>(state: _IDynamicConfigHandlerState<T>, target: C, name: string, value: V, inPlace?: boolean, rdOnly?: boolean): V {
if (target) {
let isDynamic = false;
// To be a dynamic property it needs to have a get function
let propDesc = objGetOwnPropertyDescriptor(target, name);
if (propDesc && propDesc.get) {
isDynamic = !!propDesc.get[state.prop];
}
let getter = _getOwnPropGetter(target, name);
let isDynamic = getter && !!getter[state.prop];
if (!isDynamic) {
_makeDynamicProperty(state, target, name, value);
value = _makeDynamicProperty(state, target, name, value);
if (inPlace || rdOnly) {
getter = _getOwnPropGetter(target, name);
}
} else {
// Looks like it's already dynamic and a different value so just assign the new value
// Looks like it's already dynamic just assign the new value
target[name] = value;
}
// Assign the optional flags if true
if (inPlace) {
getter[state.rf] = inPlace;
}
if (rdOnly) {
getter[state.ro] = rdOnly;
}
}
return value;

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

@ -11,26 +11,10 @@ import { _IDynamicConfigHandlerState } from "./_IDynamicConfigHandlerState";
const symPrefix = "[[ai_";
const symPostfix = "]]";
/**
* @internal
* @ignore
*/
interface _IWaitingHandlers<T> {
/**
* The handler to be re-executed
*/
h: IWatcherHandler<T>;
/**
* The detail objects that caused this handler to be re-executed, each property has a detail instance
* but may be the same handler
*/
//d: _IDynamicDetail<T>[];
}
export function _createState<T>(cfgHandler: _IInternalDynamicConfigHandler<T>): _IDynamicConfigHandlerState<T> {
let dynamicPropertySymbol = newSymbol(symPrefix + "get" + cfgHandler.uid + symPostfix);
let dynamicPropertyReadOnly = newSymbol(symPrefix + "ro" + cfgHandler.uid + symPostfix);
let dynamicPropertyReferenced = newSymbol(symPrefix + "rf" + cfgHandler.uid + symPostfix);
let dynamicPropertyDetail = newSymbol(symPrefix + "dtl" + cfgHandler.uid + symPostfix);
let _waitingHandlers: IWatcherHandler<T>[] = null;
let _watcherTimer: ITimerHandler = null;
@ -52,7 +36,9 @@ export function _createState<T>(cfgHandler: _IInternalDynamicConfigHandler<T>):
callback({
cfg: cfgHandler.cfg,
set: cfgHandler.set.bind(cfgHandler),
setDf: cfgHandler.setDf.bind(cfgHandler)
setDf: cfgHandler.setDf.bind(cfgHandler),
ref: cfgHandler.ref.bind(cfgHandler),
rdOnly: cfgHandler.rdOnly.bind(cfgHandler)
});
} catch(e) {
let logger = cfgHandler.logger;
@ -156,6 +142,7 @@ export function _createState<T>(cfgHandler: _IInternalDynamicConfigHandler<T>):
theState = {
prop: dynamicPropertySymbol,
ro: dynamicPropertyReadOnly,
rf: dynamicPropertyReferenced,
hdlr: cfgHandler,
add: _addWatcher,
notify: _notifyWatchers,

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

@ -49,6 +49,22 @@ export interface IDynamicConfigHandler<T extends IConfiguration> {
* @param defaultValues - The default values to apply to the config
*/
setDf: <C>(theConfig: C, defaultValues: IConfigDefaults<C, T>) => C;
/**
* Set this named property of the target as referenced, which will cause any object or array instances
* to be updated in-place rather than being entirely replaced. All other values will continue to be replaced.
* @returns The referenced properties current value
*/
ref: <C, V = any>(target: C, name: string) => V;
/**
* Set this named property of the target as read-only, which will block this single named property from
* ever being changed for the target instance.
* This does NOT freeze or seal the instance, it just stops the direct re-assignment of the named property,
* if the value is a non-primitive (ie. an object or array) it's properties will still be mutable.
* @returns The referenced properties current value
*/
rdOnly: <C, V = any>(target: C, name: string) => V;
}
/**

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

@ -26,6 +26,22 @@ export interface IWatchDetails<T extends IConfiguration> {
* @param defaultValues - The default values to apply to the config
*/
setDf: <C>(theConfig: C, defaultValues: IConfigDefaults<C>) => C;
/**
* Set this named property of the target as referenced, which will cause any object or array instance
* to be updated in-place rather than being entirely replaced. All other values will continue to be replaced.
* @returns The referenced properties current value
*/
ref: <C, V = any>(target: C, name: string) => V;
/**
* Set this named property of the target as read-only, which will block this single named property from
* ever being changed for the target instance.
* This does NOT freeze or seal the instance, it just stops the direct re-assignment of the named property,
* if the value is a non-primitive (ie. an object or array) it's properties will still be mutable.
* @returns The referenced properties current value
*/
rdOnly: <C, V = any>(target: C, name: string) => V;
}
export type WatcherFunction<T extends IConfiguration> = (details: IWatchDetails<T>) => void;

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

@ -20,8 +20,9 @@ export interface _IDynamicGetter {
* Interface for the global dynamic config handler
*/
export interface _IDynamicConfigHandlerState<T> {
prop: symbol;
ro: symbol;
prop: symbol; // Identify that this is a dynamic property
ro: symbol; // Identify that this property is read-only
rf: symbol; // Identify that this property is referenced and should be updated in-place
/**
* Link to the handler

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

@ -4,8 +4,8 @@
import dynamicProto from "@microsoft/dynamicproto-js";
import {
ITimerHandler, arrAppend, arrForEach, arrIndexOf, deepExtend, dumpObj, hasDocument, isFunction, isNullOrUndefined, isPlainObject,
objDeepFreeze, objDefineProp, objForEachKey, objFreeze, objHasOwn, scheduleInterval, scheduleTimeout, throwError
ITimerHandler, arrAppend, arrForEach, arrIndexOf, deepExtend, hasDocument, isFunction, isNullOrUndefined, isPlainObject, objDeepFreeze,
objDefineProp, objForEachKey, objFreeze, objHasOwn, scheduleInterval, scheduleTimeout, throwError
} from "@nevware21/ts-utils";
import { createDynamicConfig, onConfigChange } from "../Config/DynamicConfig";
import { IConfigDefaults } from "../Config/IConfigDefaults";
@ -50,6 +50,7 @@ import {
import { _getPluginState, createDistributedTraceContext, initializePlugins, sortPlugins } from "./TelemetryHelpers";
import { TelemetryInitializerPlugin } from "./TelemetryInitializerPlugin";
import { IUnloadHandlerContainer, UnloadHandler, createUnloadHandlerContainer } from "./UnloadHandlerContainer";
import { IUnloadHookContainer, createUnloadHookContainer } from "./UnloadHookContainer";
const strValidationError = "Plugins must provide initialize method";
const strNotificationManager = "_notificationManager";
@ -246,7 +247,7 @@ export class AppInsightsCore implements IAppInsightsCore {
let _internalLogsEventName: string | null;
let _evtNamespace: string;
let _unloadHandlers: IUnloadHandlerContainer;
let _hooks: Array<ILegacyUnloadHook | IUnloadHook>;
let _hookContainer: IUnloadHookContainer;
let _debugListener: INotificationListener | null;
let _traceCtx: IDistributedTraceContext | null;
let _instrumentationKey: string | null;
@ -293,6 +294,14 @@ export class AppInsightsCore implements IAppInsightsCore {
_addUnloadHook(_configHandler.watch((details) => {
_instrumentationKey = details.cfg.instrumentationKey;
// Mark the extensionConfig and all first level keys as referenced
// This is so that calls to getExtCfg() will always return the same object
// Even when a user may "re-assign" the plugin properties (or it's unloaded/reloaded)
let extCfg = details.ref(details.cfg, STR_EXTENSION_CONFIG);
objForEachKey(extCfg, (key) => {
details.ref(extCfg, key);
});
if (isNullOrUndefined(_instrumentationKey)) {
throwError("Please provide instrumentation key");
}
@ -527,18 +536,7 @@ export class AppInsightsCore implements IAppInsightsCore {
let processUnloadCtx = createProcessTelemetryUnloadContext(_getPluginChain(), _self);
processUnloadCtx.onComplete(() => {
let oldHooks = _hooks;
_hooks = [];
// Remove all registered unload hooks
arrForEach(oldHooks, (fn) => {
// allow either rm or remove callback function
try{
((fn as IUnloadHook).rm || (fn as ILegacyUnloadHook).remove).call(fn);
} catch (e) {
_throwInternal(_self.logger, eLoggingSeverity.WARNING, _eInternalMessageId.PluginException, "Unloading:" + dumpObj(e));
}
});
_hookContainer.run(_self.logger);
_initDefaults();
unloadComplete && unloadComplete(unloadState);
@ -800,7 +798,7 @@ export class AppInsightsCore implements IAppInsightsCore {
_unloadHandlers = createUnloadHandlerContainer();
_traceCtx = null;
_instrumentationKey = null;
_hooks = [];
_hookContainer = createUnloadHookContainer();
_cfgListeners = [];
_pluginVersionString = null;
_pluginVersionStringArr = null;
@ -1103,9 +1101,7 @@ export class AppInsightsCore implements IAppInsightsCore {
}
function _addUnloadHook(hooks: IUnloadHook | IUnloadHook[] | Iterator<IUnloadHook> | ILegacyUnloadHook | ILegacyUnloadHook[] | Iterator<ILegacyUnloadHook>) {
if (hooks) {
arrAppend(_hooks, hooks);
}
_hookContainer.add(hooks);
}
});
}

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

@ -3,10 +3,9 @@
"use strict";
import dynamicProto from "@microsoft/dynamicproto-js";
import { arrAppend, arrForEach, dumpObj, isFunction } from "@nevware21/ts-utils";
import { isFunction, objDefine } from "@nevware21/ts-utils";
import { createDynamicConfig } from "../Config/DynamicConfig";
import { IConfigDefaults } from "../Config/IConfigDefaults";
import { _eInternalMessageId, eLoggingSeverity } from "../JavaScriptSDK.Enums/LoggingEnums";
import { TelemetryUnloadReason } from "../JavaScriptSDK.Enums/TelemetryUnloadReason";
import { TelemetryUpdateReason } from "../JavaScriptSDK.Enums/TelemetryUpdateReason";
import { IAppInsightsCore } from "../JavaScriptSDK.Interfaces/IAppInsightsCore";
@ -21,18 +20,18 @@ import { ITelemetryPluginChain } from "../JavaScriptSDK.Interfaces/ITelemetryPlu
import { ITelemetryUnloadState } from "../JavaScriptSDK.Interfaces/ITelemetryUnloadState";
import { ITelemetryUpdateState } from "../JavaScriptSDK.Interfaces/ITelemetryUpdateState";
import { ILegacyUnloadHook, IUnloadHook } from "../JavaScriptSDK.Interfaces/IUnloadHook";
import { _throwInternal } from "./DiagnosticLogger";
import { isNotNullOrUndefined, proxyFunctionAs } from "./HelperFuncs";
import { STR_EXTENSION_CONFIG } from "./InternalConstants";
import {
createProcessTelemetryContext, createProcessTelemetryUnloadContext, createProcessTelemetryUpdateContext
} from "./ProcessTelemetryContext";
import { IUnloadHandlerContainer, UnloadHandler, createUnloadHandlerContainer } from "./UnloadHandlerContainer";
import { IUnloadHookContainer, createUnloadHookContainer } from "./UnloadHookContainer";
let strGetPlugin = "getPlugin";
const defaultValues: IConfigDefaults<IConfiguration> = {
[STR_EXTENSION_CONFIG]: { isVal: isNotNullOrUndefined, v: []}
[STR_EXTENSION_CONFIG]: { isVal: isNotNullOrUndefined, v: {} }
};
@ -110,6 +109,13 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
*/
protected _doUpdate?: (updateCtx?: IProcessTelemetryUpdateContext, updateState?: ITelemetryUpdateState, asyncCallback?: () => void) => void | boolean;
/**
* Exposes the underlying unload hook container instance for this extension to allow it to be passed down to any sub components of the class.
* This should NEVER be exposed or called publically as it's scope is for internal use by BaseTelemetryPlugin and any derived class (which is why
* it's scoped as protected)
*/
protected readonly _unloadHooks: IUnloadHookContainer;
constructor() {
let _self = this; // Setting _self here as it's used outside of the dynamicProto as well
@ -118,7 +124,7 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
let _rootCtx: IProcessTelemetryContext; // Used as the root context, holding the current config and initialized core
let _nextPlugin: ITelemetryPlugin | ITelemetryPluginChain; // Used for backward compatibility where plugins don't call the main pipeline
let _unloadHandlerContainer: IUnloadHandlerContainer;
let _hooks: Array<ILegacyUnloadHook| IUnloadHook>;
let _hookContainer: IUnloadHookContainer;
_initDefaults();
@ -151,19 +157,7 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
unloadDone = true;
_unloadHandlerContainer.run(theUnloadCtx, unloadState);
let oldHooks = _hooks;
_hooks = [];
// Remove all registered unload hooks
arrForEach(oldHooks, (fn) => {
// allow either rm or remove callback function
try{
((fn as IUnloadHook).rm || (fn as ILegacyUnloadHook).remove).call(fn);
} catch (e) {
_throwInternal(theUnloadCtx.diagLog(), eLoggingSeverity.WARNING, _eInternalMessageId.PluginException, "Unloading:" + dumpObj(e));
}
});
_hookContainer.run(theUnloadCtx.diagLog());
if (result === true) {
theUnloadCtx.processNext(theUnloadState);
@ -215,13 +209,9 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
return result;
};
_self._addHook = (hooks: IUnloadHook | IUnloadHook[] | ILegacyUnloadHook | ILegacyUnloadHook[]) => {
if (hooks) {
arrAppend(_hooks, hooks);
}
};
proxyFunctionAs(_self, "_addUnloadCb", () => _unloadHandlerContainer, "add");
proxyFunctionAs(_self, "_addHook", () => _hookContainer, "add");
objDefine(_self, "_unloadHooks" as keyof BaseTelemetryPlugin, { g: () => _hookContainer });
});
// These are added after the dynamicProto so that are not moved to the prototype
@ -301,7 +291,7 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
_self.core = null;
_rootCtx = null;
_nextPlugin = null;
_hooks = [];
_hookContainer = createUnloadHookContainer();
_unloadHandlerContainer = createUnloadHandlerContainer();
}
}
@ -347,7 +337,7 @@ export abstract class BaseTelemetryPlugin implements ITelemetryPlugin {
* Add this hook so that it is automatically removed during unloading
* @param hooks - The single hook or an array of IInstrumentHook objects
*/
protected _addHook(hooks: IUnloadHook | IUnloadHook[] | ILegacyUnloadHook | ILegacyUnloadHook[]): void {
protected _addHook(hooks: IUnloadHook | IUnloadHook[] | Iterator<IUnloadHook> | ILegacyUnloadHook | ILegacyUnloadHook[] | Iterator<ILegacyUnloadHook>): void {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
}
}

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

@ -162,19 +162,22 @@ function _createInternalContext<T extends IBaseProcessingContext>(telemetryChain
extCfg = {};
}
// Always set the value so that it's created as a dynamic config element (if not already)
dynamicHandler.set(cfg, STR_EXTENSION_CONFIG, extCfg); // Note: it is valid for the "value" to be undefined
extCfg = cfg.extensionConfig;
// Always set the value so that the property always exists
cfg[STR_EXTENSION_CONFIG] = extCfg; // Note: it is valid for the "value" to be undefined
// Calling `ref()` has a side effect of causing the referenced property to become dynamic (if not already)
extCfg = dynamicHandler.ref(cfg, STR_EXTENSION_CONFIG);
if (extCfg) {
idCfg = extCfg[identifier];
if (!idCfg && createIfMissing) {
idCfg = {} as T;
}
// Always set the value so that it's created as a dynamic config element (if not already)
dynamicHandler.set(extCfg, identifier, idCfg);
idCfg = extCfg[identifier];
// Always set the value so that the property always exists
extCfg[identifier] = idCfg; // Note: it is valid for the "value" to be undefined
// Calling `ref()` has a side effect of causing the referenced property to become dynamic (if not already)
idCfg = dynamicHandler.ref(extCfg, identifier);
}
}

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

@ -14,7 +14,7 @@ export interface IUnloadHandlerContainer {
run: (itemCtx: IProcessTelemetryUnloadContext, unloadState: ITelemetryUnloadState) => void
}
export function createUnloadHandlerContainer() {
export function createUnloadHandlerContainer(): IUnloadHandlerContainer {
let handlers: UnloadHandler[] = [];
function _addHandler(handler: UnloadHandler) {

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

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { arrAppend, arrForEach, dumpObj } from "@nevware21/ts-utils";
import { eLoggingSeverity, _eInternalMessageId } from "../JavaScriptSDK.Enums/LoggingEnums";
import { IDiagnosticLogger } from "../JavaScriptSDK.Interfaces/IDiagnosticLogger";
import { ILegacyUnloadHook, IUnloadHook } from "../JavaScriptSDK.Interfaces/IUnloadHook";
import { _throwInternal } from "./DiagnosticLogger";
/**
* Interface which identifiesAdd this hook so that it is automatically removed during unloading
* @param hooks - The single hook or an array of IInstrumentHook objects
*/
export interface IUnloadHookContainer {
add: (hooks: IUnloadHook | IUnloadHook[] | Iterator<IUnloadHook> | ILegacyUnloadHook | ILegacyUnloadHook[] | Iterator<ILegacyUnloadHook>) => void;
run: (logger?: IDiagnosticLogger) => void;
}
/**
* Create a IUnloadHookContainer which can be used to remember unload hook functions to be executed during the component unloading
* process.
* @returns A new IUnloadHookContainer instance
*/
export function createUnloadHookContainer(): IUnloadHookContainer {
let _hooks: Array<ILegacyUnloadHook | IUnloadHook> = [];
function _doUnload(logger: IDiagnosticLogger) {
let oldHooks = _hooks;
_hooks = [];
// Remove all registered unload hooks
arrForEach(oldHooks, (fn) => {
// allow either rm or remove callback function
try{
((fn as IUnloadHook).rm || (fn as ILegacyUnloadHook).remove).call(fn);
} catch (e) {
_throwInternal(logger, eLoggingSeverity.WARNING, _eInternalMessageId.PluginException, "Unloading:" + dumpObj(e));
}
});
}
function _addHook(hooks: IUnloadHook | IUnloadHook[] | Iterator<IUnloadHook> | ILegacyUnloadHook | ILegacyUnloadHook[] | Iterator<ILegacyUnloadHook>) {
if (hooks) {
arrAppend(_hooks, hooks);
}
}
return {
run: _doUnload,
add: _addHook
};
}

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

@ -80,6 +80,7 @@ export { getDebugListener, getDebugExt } from "./JavaScriptSDK/DbgExtensionUtils
export { TelemetryInitializerFunction, ITelemetryInitializerHandler, ITelemetryInitializerContainer } from "./JavaScriptSDK.Interfaces/ITelemetryInitializers";
export { createUniqueNamespace } from "./JavaScriptSDK/DataCacheHelper";
export { UnloadHandler, IUnloadHandlerContainer, createUnloadHandlerContainer } from "./JavaScriptSDK/UnloadHandlerContainer";
export { IUnloadHookContainer, createUnloadHookContainer } from "./JavaScriptSDK/UnloadHookContainer";
export { ITelemetryUpdateState } from "./JavaScriptSDK.Interfaces/ITelemetryUpdateState";
export { ITelemetryUnloadState } from "./JavaScriptSDK.Interfaces/ITelemetryUnloadState";
export { IDistributedTraceContext } from "./JavaScriptSDK.Interfaces/IDistributedTraceContext";