UNPKG

pxt-core

Version:

Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors

352 lines (315 loc) • 14.4 kB
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; let partnerName: string; 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>>; type EventListener<T = any> = (ev: T) => void; type EventSource<T> = { subscribe(listener: (ev: T) => void): () => void; emit(ev: T): void; forEach(callback: (ev: T) => void): void; }; function createEventSource<T = any>(filter?: ((ev: T) => boolean)): EventSource<T> { filter = filter || (() => true); const listeners: EventListener<T>[] = []; const eventCache: T[] = []; return { subscribe(listener: EventListener<T>): () => void { eventCache.forEach(ev => filter(ev) && listener(ev)); listeners.push(listener); // Return an unsubscribe function return () => { const index = listeners.indexOf(listener); if (index !== -1) { listeners.splice(index, 1); } }; }, emit(ev: T): void { eventCache.push(ev); if (filter(ev)) listeners.forEach(listener => listener(ev)); }, forEach(callback: (ev: T) => void): void { eventCache.forEach(ev => filter(ev) && callback(ev)); } }; } // 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 measurementThresholdMs = 10; export let stats: { durations: EventSource<{ name: string, start: number, duration: number, params?: Map<string> }>, milestones: EventSource<{ milestone: string, time: number, params?: Map<string> }>, } = { durations: createEventSource((ev) => ev.duration >= measurementThresholdMs), milestones: createEventSource(), } export function isEnabled() { return enabled; } 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, params?: Map<string>) { const time = splitMs() stats.milestones.emit({ milestone: msg, time, params }); } 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, params?: Map<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 stats.durations.emit({ name, start: measure.startTime, duration: durMs, params }); } performance.clearMarks(`${name} start`) performance.clearMarks(`${name} end`) performance.clearMeasures(`${name} elapsed`) } } export function report() { perfReportLogged = true; if (enabled) { const milestones: { [index: string]: number } = {}; const durations: { [index: string]: number } = {}; let report = `Performance Report:\n` report += `\n` report += `\tMilestones:\n` stats.milestones.forEach(({ milestone, time, params }) => { let pretty = prettyStr(time) report += `\t\t${milestone} @ ${pretty}` for (let k of Object.keys(params || {})) { report += `\n\t\t\t${k}: ${params[k]}` } report += `\n` milestones[milestone] = time; }); report += `\n` report += `\tMeasurements:\n` stats.durations.forEach(({ name, start, duration, params }) => { let pretty = prettyStr(duration) report += `\t\t${name} took ~ ${pretty}` report += ` (${prettyStr(start)} - ${prettyStr(start + duration)})` for (let k of Object.keys(params || {})) { report += `\n\t\t\t${k}: ${params[k]}` } report += `\n` durations[name] = duration; }); console.log(report) enabled = false; // stop collecting milestones and measurements after report return { milestones, durations }; } return undefined; } (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({ name: a, properties: b, measurements: 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({ exception: a, properties: b ? { ...c, ["kind"]: b } : c, }) ); } exceptionLogger.track(err, kind, props); } export function initializeAppInsightsInternal(includeCookie = false) { try { const params = new URLSearchParams(window.location.search); if (params.has("partner")) { partnerName = params.get("partner"); } } catch (e) { console.warn("Could not parse search string", e); } // 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; // App Insights automatically sends a page view event on setup, but we send our own later with additional properties. // This stops the automatic event from firing, so we don't end up with duplicate page view events. if (envelope.baseType == "PageviewData" && !envelope.baseData.properties) { return false; } if (envelope.baseType == "PageviewPerformanceData") { const pageName = envelope.baseData.name; envelope.baseData.name = window.location.origin; if (!envelope.baseData.properties) { envelope.baseData.properties = {}; } envelope.baseData.properties.pageName = pageName; envelope.baseData.properties.pathName = window.location.pathname; // no url scrubbing for webapp (no share url, etc) } if (typeof pxtConfig === "undefined" || !pxtConfig) return true; const telemetryItem = envelope.baseData; telemetryItem.properties = telemetryItem.properties || {}; telemetryItem.properties["target"] = pxtConfig.targetId; telemetryItem.properties["stage"] = (pxtConfig.relprefix || "/--").replace(/[^a-z]/ig, '') telemetryItem.properties["targetVersion"] = pxtConfig.targetVersion; telemetryItem.properties["pxtVersion"] = pxtConfig.pxtVersion; if (partnerName) { telemetryItem.properties["partner"] = partnerName; } const userAgent = navigator.userAgent.toLowerCase(); const electronRegexResult = /\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 (electronRegexResult) { telemetryItem.properties["Electron"] = 1; telemetryItem.properties["ElectronVersion"] = electronRegexResult[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; } // Kiosk UWP info is appended to the user agent by the makecode-dotnet-apps/arcade-kiosk UWP app const kioskUwpRegexResult = /\((MakeCode Arcade Kiosk UWP)\/([\S]+)\/([\S]+)\)/i.exec(userAgent); // Example navigator.userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60 (MakeCode Arcade Kiosk UWP/0.1.41.0/Windows.Xbox)" if (kioskUwpRegexResult) { telemetryItem.properties["KioskUwp"] = 1; telemetryItem.properties["KioskUwpVersion"] = kioskUwpRegexResult[2]; telemetryItem.properties["KioskUwpPlatform"] = kioskUwpRegexResult[3]; } // "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; return true; } 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; } }