pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
251 lines (226 loc) • 9.96 kB
text/typescript
declare let process: any;
namespace pxt {
type Map<T> = { [index: string]: T };
const eventBufferSizeLimit = 20;
const queues: TelemetryQueue<any, any, any>[] = [];
let analyticsLoaded = false;
let interactiveConsent = false;
let isProduction = false;
class TelemetryQueue<A, B, C> {
private q: [A, B, C][] = [];
constructor(private log: (a?: A, b?: B, c?: C) => void) {
queues.push(this);
}
public track(a: A, b: B, c: C) {
if (analyticsLoaded) {
this.log(a, b, c);
}
else {
this.q.push([a, b, c]);
if (this.q.length > eventBufferSizeLimit) this.q.shift();
}
}
public flush() {
while (this.q.length) {
const [a, b, c] = this.q.shift();
this.log(a, b, c);
}
}
}
let eventLogger: TelemetryQueue<string, Map<string>, Map<number>>;
let exceptionLogger: TelemetryQueue<any, string, Map<string>>;
// performance measuring, added here because this is amongst the first (typescript) code ever executed
export namespace perf {
let enabled: boolean;
export let startTimeMs: number;
export let stats: {
// name, start, duration
durations: [string, number, number][],
// name, event
milestones: [string, number][]
} = {
durations: [],
milestones: []
}
export let perfReportLogged = false
export function splitMs(): number {
return Math.round(performance.now() - startTimeMs)
}
export function prettyStr(ms: number): string {
ms = Math.round(ms)
let r_ms = ms % 1000
let s = Math.floor(ms / 1000)
let r_s = s % 60
let m = Math.floor(s / 60)
if (m > 0)
return `${m}m${r_s}s`
else if (s > 5)
return `${s}s`
else if (s > 0)
return `${s}s${r_ms}ms`
else
return `${ms}ms`
}
export function splitStr(): string {
return prettyStr(splitMs())
}
export function recordMilestone(msg: string, time: number = splitMs()) {
stats.milestones.push([msg, time])
}
export function init() {
enabled = performance && !!performance.mark && !!performance.measure;
if (enabled) {
performance.measure("measure from the start of navigation to now")
let navStartMeasure = performance.getEntriesByType("measure")[0]
startTimeMs = navStartMeasure.startTime
}
}
export function measureStart(name: string) {
if (enabled) performance.mark(`${name} start`)
}
export function measureEnd(name: string) {
if (enabled && performance.getEntriesByName(`${name} start`).length) {
performance.mark(`${name} end`)
performance.measure(`${name} elapsed`, `${name} start`, `${name} end`)
let e = performance.getEntriesByName(`${name} elapsed`, "measure")
if (e && e.length === 1) {
let measure = e[0]
let durMs = measure.duration
if (durMs > 10) {
stats.durations.push([name, measure.startTime, durMs])
}
}
performance.clearMarks(`${name} start`)
performance.clearMarks(`${name} end`)
performance.clearMeasures(`${name} elapsed`)
}
}
export function report(filter: string = null) {
if (enabled) {
let report = `performance report:\n`
for (let [msg, time] of stats.milestones) {
if (!filter || msg.indexOf(filter) >= 0) {
let pretty = prettyStr(time)
report += `\t\t${msg} @ ${pretty}\n`
}
}
report += `\n`
for (let [msg, start, duration] of stats.durations) {
let filterIncl = filter && msg.indexOf(filter) >= 0
if ((duration > 50 && !filter) || filterIncl) {
let pretty = prettyStr(duration)
report += `\t\t${msg} took ~ ${pretty}`
if (duration > 1000) {
report += ` (${prettyStr(start)} - ${prettyStr(start + duration)})`
}
report += `\n`
}
}
console.log(report)
}
perfReportLogged = true
}
(function () {
init()
recordMilestone("first JS running")
})()
}
export function initAnalyticsAsync() {
if (isNativeApp() || shouldHideCookieBanner()) {
initializeAppInsightsInternal(true);
return;
}
if ((window as any).pxtSkipAnalyticsCookie) {
initializeAppInsightsInternal(false);
return;
}
initializeAppInsightsInternal(true);
}
export function aiTrackEvent(id: string, data?: any, measures?: any) {
if (!eventLogger) {
eventLogger = new TelemetryQueue((a, b, c) => (window as any).appInsights.trackEvent(a, b, c));
}
eventLogger.track(id, data, measures);
}
export function aiTrackException(err: any, kind?: string, props?: any) {
if (!exceptionLogger) {
exceptionLogger = new TelemetryQueue((a, b, c) => (window as any).appInsights.trackException(a, b, c));
}
exceptionLogger.track(err, kind, props);
}
export function initializeAppInsightsInternal(includeCookie = false) {
// loadAppInsights is defined in docfiles/tracking.html
const loadAI = (window as any).loadAppInsights;
if (loadAI) {
isProduction = loadAI(includeCookie, telemetryInitializer);
analyticsLoaded = true;
queues.forEach(a => a.flush());
}
}
function telemetryInitializer(envelope: any) {
const pxtConfig = (window as any).pxtConfig;
if (typeof pxtConfig === "undefined" || !pxtConfig) return;
const telemetryItem = envelope.data.baseData;
telemetryItem.properties = telemetryItem.properties || {};
telemetryItem.properties["target"] = pxtConfig.targetId;
telemetryItem.properties["stage"] = (pxtConfig.relprefix || "/--").replace(/[^a-z]/ig, '')
const userAgent = navigator.userAgent.toLowerCase();
const userAgentRegexResult = /\belectron\/(\d+\.\d+\.\d+.*?)(?: |$)/i.exec(userAgent); // Example navigator.userAgent: "Mozilla/5.0 Chrome/61.0.3163.100 Electron/2.0.0 Safari/537.36"
if (userAgentRegexResult) {
telemetryItem.properties["Electron"] = 1;
telemetryItem.properties["ElectronVersion"] = userAgentRegexResult[1];
}
const pxtElectron = (window as any).pxtElectron;
if (typeof pxtElectron !== "undefined") {
telemetryItem.properties["PxtElectron"] = 1;
telemetryItem.properties["ElectronVersion"] = pxtElectron.versions.electronVersion;
telemetryItem.properties["ChromiumVersion"] = pxtElectron.versions.chromiumVersion;
telemetryItem.properties["NodeVersion"] = pxtElectron.versions.nodeVersion;
telemetryItem.properties["PxtElectronVersion"] = pxtElectron.versions.pxtElectronVersion;
telemetryItem.properties["PxtCoreVersion"] = pxtElectron.versions.pxtCoreVersion;
telemetryItem.properties["PxtTargetVersion"] = pxtElectron.versions.pxtTargetVersion;
telemetryItem.properties["PxtElectronIsProd"] = pxtElectron.versions.isProd;
}
// "cookie" does not actually correspond to whether or not we drop the cookie because we recently
// switched to immediately dropping it rather than waiting. Instead, we maintain the legacy behavior
// of only setting it to true for production sites where interactive consent has been obtained
// so that we don't break legacy queries
telemetryItem.properties["cookie"] = interactiveConsent && isProduction;
}
export function setInteractiveConsent(enabled: boolean) {
interactiveConsent = enabled;
}
/**
* Checks for pxt-electron and Code Connection
*/
function isNativeApp(): boolean {
const hasWindow = typeof window !== "undefined";
const isPxtElectron = hasWindow && !!(window as any).pxtElectron;
const isCC = hasWindow && !!(window as any).ipcRenderer || /ipc=1/.test(location.hash) || /ipc=1/.test(location.search); // In WKWebview, ipcRenderer is injected later, so use the URL query
return isPxtElectron || isCC;
}
/**
* Checks whether we should hide the cookie banner
*/
function shouldHideCookieBanner(): boolean {
//We don't want a cookie notification when embedded in editor controllers, we'll use the url to determine that
const noCookieBanner = isIFrame() && /nocookiebanner=1/i.test(window.location.href)
return noCookieBanner;
}
function isIFrame(): boolean {
try {
return window && window.self !== window.top;
} catch (e) {
return false;
}
}
/**
* checks for sandbox
*/
function isSandboxMode(): boolean {
//This is restricted set from pxt.shell.isSandBoxMode and specific to share page
//We don't want cookie notification in the share page
const sandbox = /sandbox=1|#sandbox|#sandboxproject/i.test(window.location.href)
return sandbox;
}
}