Skip to content

Commit f5410a9

Browse files
committed
Improve OsPlugin reliability: proactive OS retrieval, unload handling, and session caching
- Refactor OsPlugin to start OS version retrieval during initialize() instead of waiting for the first telemetry event, reducing latency for the initial tracked events - Add page unload/pagehide/visibilitychange event handlers to flush the queued telemetry when the page is being unloaded before the OS lookup completes - Respect disableFlushOnUnload config to skip registering unload handlers when disabled - Properly clean up unload event handlers after OS version is resolved - Improve session storage caching: validate cached OS data on load and guard writes behind isStorageUseDisabled config check - Use safeGetLogger and getNavigator() instead of direct navigator/core.logger access for safer operation when core may not be fully initialized - Fix safeGetLogger to fall back to core.config when no explicit config is provided
1 parent a598f47 commit f5410a9

6 files changed

Lines changed: 513 additions & 229 deletions

File tree

.aiAutoMinify.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"TelemetryUnloadReason",
2727
"TelemetryUpdateReason",
2828
"eTraceHeadersMode",
29+
"eUrlRedactionOptions",
2930
"eValueKind",
3031
"EventLatencyValue",
3132
"eEventPropertyType",

extensions/applicationinsights-osplugin-js/Tests/Unit/src/OsPluginTest.ts

Lines changed: 309 additions & 92 deletions
Large diffs are not rendered by default.

extensions/applicationinsights-osplugin-js/src/OsPlugin.ts

Lines changed: 163 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,33 @@
66
import dynamicProto from "@microsoft/dynamicproto-js";
77
import {
88
BaseTelemetryPlugin, Extensions, IAppInsightsCore, IConfig, IConfigDefaults, IConfiguration, IPlugin, IProcessTelemetryContext,
9-
IProcessTelemetryUnloadContext, ITelemetryItem, ITelemetryUnloadState, _eInternalMessageId, _throwInternal, addPageHideEventListener,
9+
IProcessTelemetryUnloadContext, ITelemetryItem, ITelemetryUnloadState, Undefined, _eInternalMessageId, _throwInternal, addPageHideEventListener,
1010
addPageUnloadEventListener, arrForEach, createProcessTelemetryContext, createUniqueNamespace, eLoggingSeverity, getSetValue,
11-
mergeEvtNamespace, onConfigChange, removePageHideEventListener, removePageUnloadEventListener, setValue, utlCanUseSessionStorage,
11+
mergeEvtNamespace, onConfigChange, removePageHideEventListener, removePageUnloadEventListener, safeGetLogger, setValue,
1212
utlGetSessionStorage, utlSetSessionStorage
1313
} from "@microsoft/applicationinsights-core-js";
1414
import { IPromise, doAwaitResponse } from "@nevware21/ts-async";
15-
import { ITimerHandler, isString, objDeepFreeze, scheduleTimeout } from "@nevware21/ts-utils";
15+
import { ITimerHandler, asString, fnCall, getNavigator, isString, objDeepFreeze, scheduleTimeout } from "@nevware21/ts-utils";
1616
import { IOSPluginConfiguration } from "./DataModels";
1717

1818
const defaultMaxTimeout = 200;
1919
const strExt = "ext";
20+
2021
interface platformVersionInterface {
2122
platform?: string,
2223
platformVersion?: string
2324
}
25+
2426
interface UserAgentHighEntropyData {
2527
platformVersion: platformVersionInterface
2628
}
27-
interface ModernNavigator {
29+
30+
interface ModernNavigator extends Navigator {
2831
userAgentData?: {
2932
getHighEntropyValues?: (fields: ["platformVersion"]) => IPromise<UserAgentHighEntropyData>;
3033
};
31-
}
34+
}
35+
3236
const defaultOSConfig: IConfigDefaults<IOSPluginConfiguration> = objDeepFreeze({
3337
maxTimeout: defaultMaxTimeout,
3438
mergeOsNameVersion: undefined
@@ -41,28 +45,26 @@ interface IDelayedEvent {
4145

4246
export class OsPlugin extends BaseTelemetryPlugin {
4347
public identifier = "OsPlugin";
44-
public priority = 195;
48+
public priority = 195; // Note: we want this to run after the AnalyticsPlugin so that it correctly sets whether we are allowed to use session storage
4549
public version = "#version#";
4650

4751
constructor() {
4852
super();
4953
let _core: IAppInsightsCore;
5054
let _ocConfig: IOSPluginConfiguration;
51-
let _getOSInProgress: boolean;
52-
let _getOSTimeout: ITimerHandler;
53-
let _maxTimeout: number;
55+
let _getOSTimeout: ITimerHandler | null;
5456

55-
let _platformVersionResponse: platformVersionInterface;
56-
let _retrieveFullVersion: boolean;
57+
let _fetchedFullVersion: boolean;
5758
let _mergeOsNameVersion: boolean;
5859

5960
let _eventQueue: IDelayedEvent[];
6061
let _evtNamespace: string | string[];
61-
let _excludePageUnloadEvents: string[];
62+
let _excludePageUnloadEvents: string[] | null;
63+
let _disableFlushOnUnload: boolean;
64+
let _addedUnloadEvents: boolean;
6265

63-
let _os: string;
64-
let _osVer: number;
65-
let _firstAttempt: boolean;
66+
let _os: string | undefined | null;
67+
let _osVer: number | undefined | null;
6668

6769
dynamicProto(OsPlugin, this, (_self, _base) => {
6870

@@ -73,130 +75,192 @@ export class OsPlugin extends BaseTelemetryPlugin {
7375
_core = core;
7476
super.initialize(coreConfig, core, extensions);
7577
let identifier = _self.identifier;
78+
7679
_evtNamespace = mergeEvtNamespace(createUniqueNamespace(identifier), core.evtNamespace && core.evtNamespace());
77-
if (utlCanUseSessionStorage) {
78-
try {
79-
_platformVersionResponse = JSON.parse(utlGetSessionStorage(core.logger, "ai_osplugin"));
80-
} catch (error) {
81-
// do nothing
82-
}
83-
}
84-
if(_platformVersionResponse){
85-
_retrieveFullVersion = true;
86-
_osVer = parseInt(_platformVersionResponse.platformVersion);
87-
_os = _platformVersionResponse.platform;
88-
}
80+
_fetchedFullVersion = _fetchCachedOSVersion(coreConfig);
81+
8982
_self._addHook(onConfigChange(coreConfig, (details)=> {
9083
let coreConfig = details.cfg;
9184
let ctx = createProcessTelemetryContext(null, coreConfig, core);
85+
9286
_ocConfig = ctx.getExtCfg<IOSPluginConfiguration>(identifier, defaultOSConfig);
93-
_maxTimeout = _ocConfig.maxTimeout;
94-
if (_ocConfig.mergeOsNameVersion !== undefined){
87+
88+
if (_ocConfig.mergeOsNameVersion !== undefined) {
9589
_mergeOsNameVersion = _ocConfig.mergeOsNameVersion;
9690
} else if (core.getPlugin("Sender").plugin){
9791
_mergeOsNameVersion = true;
9892
} else {
9993
_mergeOsNameVersion = false;
10094
}
101-
95+
10296
let excludePageUnloadEvents = coreConfig.disablePageUnloadEvents || [];
97+
let disableFlushOnUnload = coreConfig.disableFlushOnUnload || false;
98+
let removeEvents = _excludePageUnloadEvents && _excludePageUnloadEvents !== excludePageUnloadEvents;
99+
100+
if (_disableFlushOnUnload !== disableFlushOnUnload) {
101+
removeEvents = true;
102+
}
103103

104-
if (_excludePageUnloadEvents && _excludePageUnloadEvents !== excludePageUnloadEvents) {
105-
removePageUnloadEventListener(null, _evtNamespace);
106-
removePageHideEventListener(null, _evtNamespace);
104+
if (removeEvents && _addedUnloadEvents) {
105+
_removeUnloadHandlers();
107106
_excludePageUnloadEvents = null;
108107
}
109-
110-
if (!_excludePageUnloadEvents) {
111-
// If page is closed release queue
112-
addPageUnloadEventListener(_doUnload, excludePageUnloadEvents, _evtNamespace);
113-
addPageHideEventListener(_doUnload, excludePageUnloadEvents, _evtNamespace);
108+
109+
if (!_excludePageUnloadEvents && !disableFlushOnUnload) {
110+
_addUnloadHandlers(excludePageUnloadEvents);
114111
}
112+
115113
_excludePageUnloadEvents = excludePageUnloadEvents;
114+
_disableFlushOnUnload = disableFlushOnUnload;
116115
}));
117-
function _doUnload() {
118-
_releaseEventQueue();
116+
117+
// Automatically start retrieving OS version without waiting for the first telemetry event
118+
if (!_fetchedFullVersion) {
119+
// Start Requesting OS version process
120+
_startRetrieveOsVersion(_ocConfig.maxTimeout as number);
119121
}
120122
};
121123

122124
_self.processTelemetry = (event: ITelemetryItem, itemCtx?: IProcessTelemetryContext) => {
123125
itemCtx = _self._getTelCtx(itemCtx);
124126

125-
if (!_retrieveFullVersion && !_getOSInProgress && _firstAttempt) {
126-
// Start Requesting OS version process
127-
_getOSInProgress = true;
128-
startRetrieveOsVersion();
129-
_firstAttempt = false;
130-
}
131-
132-
if (_getOSInProgress) {
127+
if (_getOSTimeout) {
128+
// We have a timer waiting for the OS version to be retrieved, queue the event
133129
_eventQueue.push({
134130
ctx: itemCtx,
135131
item: event
136132
});
137133
} else {
138-
updateTeleItemWithOs(event);
134+
_updateTeleItemWithOs(event);
139135
_self.processNext(event, itemCtx);
140136
}
141137
};
142138

143139
_self._doTeardown = (unloadCtx?: IProcessTelemetryUnloadContext, unloadState?: ITelemetryUnloadState) => {
144140
_completeOsRetrieve();
145-
removePageUnloadEventListener(null, _evtNamespace);
146-
removePageHideEventListener(null, _evtNamespace);
141+
_removeUnloadHandlers();
142+
147143
// Just register to remove all events associated with this namespace
148144
_initDefaults();
149145
};
150146

151-
147+
function _fetchCachedOSVersion(coreConfig: IConfiguration & IConfig) {
148+
let fetched = false;
149+
150+
// Special case check for if the runtime doesn't include the AnalyticsPlugin
151+
if(coreConfig.isStorageUseDisabled !== true) {
152+
try {
153+
let platformVersionResponse: platformVersionInterface = JSON.parse(utlGetSessionStorage(safeGetLogger(_core), "ai_osplugin")) as platformVersionInterface;
154+
if (platformVersionResponse) {
155+
_os = platformVersionResponse.platform;
156+
if (platformVersionResponse.platformVersion) {
157+
let ver = parseInt(platformVersionResponse.platformVersion);
158+
if (!isNaN(ver)) {
159+
_osVer = ver;
160+
}
161+
}
162+
163+
fetched = !!(_os && _osVer);
164+
}
165+
} catch (error) {
166+
// do nothing
167+
}
168+
}
169+
170+
return fetched;
171+
}
172+
173+
function _storeCachedOSVersion(coreConfig: IConfiguration & IConfig) {
174+
// Special case check for if the runtime doesn't include the AnalyticsPlugin
175+
if(coreConfig.isStorageUseDisabled !== true) {
176+
try {
177+
utlSetSessionStorage(safeGetLogger(_core), "ai_osplugin", JSON.stringify({platform: _os, platformVersion: _osVer}));
178+
} catch (error) {
179+
// do nothing
180+
}
181+
}
182+
}
183+
184+
function _addUnloadHandlers(excludePageUnloadEvents?: string[]) {
185+
function _unloading() {
186+
_releaseEventQueue();
187+
_removeUnloadHandlers();
188+
}
189+
190+
// Only try and add unload handlers if we haven't already fetched the OS version
191+
if (!_addedUnloadEvents && !_fetchedFullVersion) {
192+
// If page is closed release queue
193+
addPageUnloadEventListener(_unloading, excludePageUnloadEvents, _evtNamespace);
194+
addPageHideEventListener(_unloading, excludePageUnloadEvents, _evtNamespace);
195+
_addedUnloadEvents = true;
196+
}
197+
}
198+
199+
function _removeUnloadHandlers() {
200+
if (_addedUnloadEvents) {
201+
removePageUnloadEventListener(null, _evtNamespace);
202+
removePageHideEventListener(null, _evtNamespace);
203+
_addedUnloadEvents = false;
204+
}
205+
}
206+
152207
/**
153208
* Wait for the response from the browser for the OS version and store info in the session storage
154209
*/
155-
function startRetrieveOsVersion() {
156-
// Timeout request if it takes more than 5 seconds (by default)
157-
158-
_getOSTimeout = scheduleTimeout(() => {
159-
_completeOsRetrieve();
160-
}, _maxTimeout);
161-
162-
if (navigator.userAgent) {
163-
const getHighEntropyValues = (navigator as ModernNavigator).userAgentData?.getHighEntropyValues;
164-
if (getHighEntropyValues) {
165-
doAwaitResponse((navigator as ModernNavigator).userAgentData.getHighEntropyValues(["platformVersion"]), (response:any) => {
166-
if (!response.rejected) {
167-
_platformVersionResponse = response.value;
168-
_retrieveFullVersion = true;
169-
if (_platformVersionResponse.platformVersion && _platformVersionResponse.platform) {
170-
_os = _platformVersionResponse.platform;
171-
_osVer = parseInt(_platformVersionResponse.platformVersion);
172-
if (_os === "Windows"){
173-
if (_osVer == 0){
174-
_osVer = 8;
175-
} else if (_osVer < 13){
176-
_osVer = 10;
177-
} else{
178-
_osVer = 11;
210+
function _startRetrieveOsVersion(maxTimeout: number) {
211+
if (_core && !_getOSTimeout) {
212+
let nav: ModernNavigator | undefined = getNavigator() as ModernNavigator | undefined;
213+
let userAgentData = (nav || {}).userAgentData;
214+
if (userAgentData) {
215+
const getHighEntropyValues = userAgentData.getHighEntropyValues;
216+
if (getHighEntropyValues) {
217+
// Timeout request if it takes more than 200 milliseconds (by default)
218+
_getOSTimeout = scheduleTimeout(() => {
219+
_completeOsRetrieve();
220+
}, maxTimeout);
221+
222+
doAwaitResponse(fnCall(getHighEntropyValues, userAgentData, ["platformVersion"]), (response: any) => {
223+
// Always mark as fetched regardless of success or failure
224+
_fetchedFullVersion = true;
225+
try {
226+
if (!response.rejected) {
227+
let platformVersionResponse = response.value;
228+
if (platformVersionResponse.platformVersion && platformVersionResponse.platform) {
229+
_os = platformVersionResponse.platform;
230+
_osVer = parseInt(platformVersionResponse.platformVersion);
231+
if (_os === "Windows" && !isNaN(_osVer)) {
232+
if (_osVer == 0){
233+
_osVer = 8;
234+
} else if (_osVer < 13){
235+
_osVer = 10;
236+
} else{
237+
_osVer = 11;
238+
}
239+
}
240+
241+
_storeCachedOSVersion((_core || {}).config as IConfig);
179242
}
243+
} else {
244+
_throwInternal(safeGetLogger(_core),
245+
eLoggingSeverity.CRITICAL,
246+
_eInternalMessageId.PluginException,
247+
"Could not retrieve operating system: " + response.reason);
180248
}
181-
utlSetSessionStorage(_core.logger, "ai_osplugin", JSON.stringify({platform: _os, platformVersion: _osVer}));
249+
} finally {
250+
_completeOsRetrieve();
182251
}
183-
} else {
184-
_throwInternal(_core.logger,
185-
eLoggingSeverity.CRITICAL,
186-
_eInternalMessageId.PluginException,
187-
"Could not retrieve operating system: " + response.reason);
188-
}
189-
_completeOsRetrieve();
190-
});
252+
});
253+
}
191254
}
192255
}
193256
}
194257

195-
function updateTeleItemWithOs(event: ITelemetryItem) {
196-
if (_retrieveFullVersion){
197-
let extOS = getSetValue(getSetValue(event, strExt), Extensions.OSExt);
258+
function _updateTeleItemWithOs(event: ITelemetryItem) {
259+
if (_fetchedFullVersion && (_os || _osVer)) {
260+
let extOS: any = getSetValue(getSetValue(event, strExt) as any, Extensions.OSExt);
198261
if (_mergeOsNameVersion){
199-
setValue(extOS, "osVer", _os + _osVer, isString);
262+
let mergedOS = (_os || "") + (_osVer ? asString(_osVer) : "");
263+
setValue(extOS, "osVer", mergedOS, isString);
200264
} else {
201265
setValue(extOS, "osVer", _osVer);
202266
setValue(extOS, "os", _os, isString);
@@ -210,8 +274,10 @@ export class OsPlugin extends BaseTelemetryPlugin {
210274
function _completeOsRetrieve() {
211275
if (_getOSTimeout) {
212276
_getOSTimeout.cancel();
277+
_getOSTimeout = null;
213278
}
214-
_getOSInProgress = false;
279+
280+
_removeUnloadHandlers();
215281
_releaseEventQueue();
216282
}
217283

@@ -220,7 +286,7 @@ export class OsPlugin extends BaseTelemetryPlugin {
220286
*/
221287
function _releaseEventQueue() {
222288
arrForEach(_eventQueue, (evt) => {
223-
updateTeleItemWithOs(evt.item);
289+
_updateTeleItemWithOs(evt.item);
224290
_self.processNext(evt.item, evt.ctx);
225291
});
226292
_eventQueue = [];
@@ -229,17 +295,18 @@ export class OsPlugin extends BaseTelemetryPlugin {
229295
function _initDefaults() {
230296
_core = null;
231297
_ocConfig = null;
232-
_getOSInProgress = false;
233298
_getOSTimeout = null;
234-
_maxTimeout = null;
235-
_retrieveFullVersion = false;
236299
_eventQueue = [];
237-
_firstAttempt = true;
300+
_os = null;
301+
_osVer = null;
302+
_fetchedFullVersion = false;
303+
_addedUnloadEvents = false;
304+
_excludePageUnloadEvents = null;
238305
}
239306

240307
// Special internal method to allow the DebugPlugin to hook embedded objects
241308
_self["_getDbgPlgTargets"] = () => {
242-
return [_platformVersionResponse, _eventQueue, _getOSInProgress];
309+
return [ { platform: _os, platformVersion: _osVer }, _eventQueue, !!_getOSTimeout];
243310
};
244311
});
245312
}

0 commit comments

Comments
 (0)