/// module TDev { // device signature: // touch-enabled // physical keyboard present // user-agent // portrait vs landscape export enum Ticks { noEvent, mainInit, mainHashChange, mainKeyEvent, mainShortcutKeyEvent, // i.e., not coming from textbox/textarea mainResetWorld, artSoundPreviewPlay, docsEdit, exportAzure, exportCordova, exportHTML5, scriptPropsIconArt, scriptPropsSplashArt, hourOfCodeDoneStep, hourOfCodeKeepCoding, hourOfCodeConfirm, hourOfCodeDone, hourOfCodeMore, hourOfCodeFinal, tutorialTranslateSplash, tutorialTranslateStep, tutorialTranslateScript, tutorialKeepEditing, tutorialMoreTutorials, tutorialNextStep, dodStart, dodDrop, dodWrongTarget, cordovaBuild, translateDocTopic, legalNoticeSignIn, legalNoticeAgree, calcAddMissingArgs, calcBackspace, calcCopy, calcCut, calcDedicatedOp, calcEdit, calcEditString, calcExtract, calcPromoteToParameter, calcGoToDef, calcFindRefs, calcUnselect, calcExtend, calcReplaceInScript, calcReplaceInSelection, calcReplaceInAction, calcInsertAssignment, calcInsertString, calcIntelliButton, calcIntelliButton0, calcIntelliButton1, calcIntelliButton2, calcIntelliButton3, calcIntelliButton4, calcIntelliButton5plus, calcIntelliLiteral, calcIntelliLocal, calcIntelliProperty, calcIntelliPropertyPrimary, calcIntelliSingleton, calcIntelliLibrary, calcIntelliResource, calcKeyboardSearch, calcMoveCursorLeft, calcMoveCursorRight, calcNewLine, calcNextIntelliTopPage, calcNextIntelliPageSearch, calcNextIntelliPage, calcNextIntelliPage0, calcNextIntelliPage1, calcNextIntelliPage2, calcNextIntelliPage3, calcNextIntelliPage4, calcNextIntelliPage5plus, calcAsync, calcAwait, calcNot, calcNumber, calcPaste, calcPickColor, calcPickNumber, calcDemoteIntoLocal, calcPromoteIntoGlobal, calcPromoteIntoField, calcRenameLocal, calcRenameProperty, calcSearchBack, calcSearchRun, calcSimplify, calcStrip, calcSpecialKey, calcStartSearch, calcStoreInVar, calcSwapBoolean, calcSwapAsync, calcIntelliAsync, calcSwitchToNormal, calcSwitchToNumber, calcTrueFalse, calcUndo, calcGoToBoxProperty, calcAddBoxProperty, calcInsertStringParamterValue, calcHelp, calcElseIf, calcUnElseIf, calcBindGlobal, calcFixItAtomic, calcEditArt, calcAddOptionalParameter, calcChangeOptionalParameter, codeAddAbove, codeAddBelow, codeShow, codeContinue, codeReturn, codeBreak, codeBoxed, codeCopy, codeCopySelection, codeDeleteSelection, codeCut, codeCutSelection, codeCycleSidePane, codeCycleSidePaneFull, codeExtractAction, codeFocusSidePane, codeFocusSidePaneFull, codeFor, codeForEach, codeHub, codeIf, codeMoveLeft, codeNewVar, codePaste, codeRun, codeResume, codeStartSelection, codeSurround, codeUndo, codeSplit, codeWhile, codeUninstallScript, codeBackFromSearch, actionPropPrivate, actionPropTest, actionPropAtomic, btnPaste, btnCopy, btnCut, btnSelect, btnTryFix, btnUndoFix, btnAddUp, btnAddDown, btnMoveLeft, btnBreakpoint, btnChangeKind, calcBtnBackspace, calcBtnUndo, calcBtnNumberKeypad, calcBtnMiscKeypad, calcBtnNormalKeypad, calcBtnApiSearch, editBtnSideSearch, chooseCancel, editTapBelow, coverageShown, coverageOpenInEditor, coverageBucketSurveyActionEdit, coverageBucketSurveyStatementEdit, coverageBucketSurveyDebugger, coverageBucketSurveyBreakpoint, coverageBucketSurveyExceededSuccessfully, profileShown, profileOpenInEditor, debuggerExit, debuggerContinue, debuggerStepIn, debuggerStepOver, debuggerStepOut, debuggerToggleBreakpoint, debuggerViewInit, debuggerPauseWall, debuggerShowValues, debuggerShowStack, debuggerGotoWall, debuggerGotoCurrent, debuggerAppLog, debuggerHelp, debuggerValueClicked, debuggerKeyboardEvent, editorTutorialClose, editorTutorialNext, editorTutorialPrevious, sideAddAction, sideAddActionTest, sideAddActionTypeDef, sideAddEvent, sideAddLibrary, sideAddPage, sideAddRecord, sideAddObject, sideAddTable, sideAddIndex, sideAddDecorator, sideAddResource, sideAddVariable, sideAddAnything, sideHelp, sideBraces, sideDebug, sideLogs, sideErrors, sideDeployWebSite, sidePlugins, sideButtonPlugin, sidePreview, sideHistory, sideHistoryGoTo, sidePaste, sideScript, sideScriptGoToDecl, sideSearch, sideSearchGoToDecl, sideSearchGoToStmt, sidePublish, sideShare, sideDisconnect, sideRun, sideCut, sideCopy, sideDelete, sideFindRefs, sideActionHeaderInit, sideCommentInit, sideParameterInit, sideResetSidePane, sideInlineActionInit, sideExport, sideAllTests, sideTestOne, sideEditLibrary, sideEditString, sideUpdate, sideUpdateOne, sideActionAddInput, sideActionAddOutput, sideMoveToLibrary, sideTutorialCancel, sideTutorialRedisplay, sideMoreOptions, appsCreateAzureWebsite, appsDeployWebsite, pluginStop, pluginRunOperation, pluginAddMore, pluginRunAnnotationOperation, searchApiSynthesis, searchApiSearchArt, searchApiSearchLib, searchApiSearchAuto, searchApiInsertArt, searchApiUploadArt, searchApiInsertLib, wallBack, // do not rename, or fix tipmanager wallJS, wallEdit, wallReplay, wallScreenshot, wallLogs, wallStop, wallStopForce, wallPause, wallResume, wallRun, wallTraces, wallAddHeart, wallRemoveHeart, commentAttach, commentBugTracking, coreRun, coreResume, coreRerun, corePublishHidden, corePublishPublic, runtimePlayAgain, runtimeBack, appUpdateAvailable, appNoUpdate, appQuickUpdate, appUpdate, editorUpdateScript, editorUpdateLibrary, editorRunWithProfiling, editorRunWithCoverage, editorRunAsMinion, viewActionInit, viewLibraryRefInit, viewRecordInit, viewScriptInit, viewVariableInit, hubCreateScript, hubCreateGame, hubCreateGroup, hubCreateList, hubJoinGroup, hubUploadPicture, hubUploadSound, hubGameTutorial, hubWinStore, hubDocs, hubFirstTutorial, hubDocsTutorial, hubBasicEditorVideo, hubBasicEditorVideoVoice, hubBasicEditorVideoSubtitles, hubBeginnersGettingStarted, hubChatSearch, hubRateTouchdevelop, hubChooseSkill, hubDevBootCamp, hubDocsApi, hubDocsWhatsNew, hubAdvancedEditorVideo, hubSeeMoreMyScripts, hubSeeMoreNewScripts, hubSeeMoreArt, hubSeeMoreUsers, hubSeeMoreCloudOther, hubSeeMoreShowcase, hubSeeMoreTopScripts, hubSeeMoreDocs, hubSeeMoreCloud, hubSeeMoreGroups, hubSeeMoreLists, hubMyScriptsShowcase, hubMyScriptsTemplate, hubNotifications, hubUpdates, hubDoUpdates, hubTests, hubShowcaseMgmt, hubBenchmarks, hubForum, hubFacebook, hubTwitter, hubYouTube, hubSettings, hubAbout, hubHelp, hubWrongTime, hubTag, hubTagFromList, hubTagSearch, hubFeedback, hubChooseWallpaper, changeSkillScriptProperties, changeSkillActionProperties, changeSkillScriptExplorer, editorSkillBlock, editorSkillClassic, editorSkillCurly, groupCodeReset, groupCodeNew, groupCodeGet, groupDelete, groupLeave, groupJoin, groupAllowAnyoneToJoin, groupRequireInvitationCodeToJoin, groupChangePicture, publishShareGroup, browseRun, browseEdit, browsePin, browseHeart, browseUnHeart, browsePublish, browseUninstall, browsePush, browseDiffBase, browseConvertToTutorial, browseConvertToLesson, browseAddScriptToList, browseUpdate, browseSendPullRequest, browsePublicationNotes, browseListBugs, browseListReleases, browseShare, browseListMyScripts, browseListNew, browseListTop, browseListShowcase, browseListForum, browseListDocs, browseListHelp, browseListGroups, browseListArt, browseListMyArt, browseListTags, browseListSearch, browseListUsers, browseListLists, browseFollowTopic, introSlideGetStarted, introSlideLimitations, introSlideCreateApps, introSlidePopCorn, introSlideLogIn, introSlideDismiss, recordAddKey, recordAddValue, recordAddAction, recordPersTemporary, recordPersLocal, recordPersCloud, recordPersPartial, recordExported, dbgEvent, dbgLogEvent, learnPlayVideo, learnBrowseDoc, offlineLoginSync, offlineLoginSyncNoToken, offlineLoginGettingStarted, offlineEnsureLogin, offlineEnsureLoginOk, translateNagDisplay, translateNagOk, benchmarksNagDisplay, benchmarksNagDismiss, benchmarksNagRunOne, benchmarksNagRunSuite, songAlbumPlay, songPlay, warningNotificationTap, crashDialogEdit, crashDialogDebug, toLibNewLib, toLibExistingLib, toLibOKtoMove, toLibAdvanceSelectedToPending, toLibMakeTheMove, toLibDiscardPending, toLibExitToEditor, collabFirstLoad, collabResume, collabRecordAst, collabRealMerge, collabPostChatMessage, collabStartCollaboration, collabStopCollaboration, codeCompile } export interface TickEvent { timestamp:number; event:Ticks; arg?:string; } export interface BugReport { exceptionConstructor: string; exceptionMessage: string; context: string; currentUrl: string; jsUrl: string; scriptId: string; stackTrace: string; sourceURL: string; line: number; eventTrace: string; userAgent: string; resolution: string; timestamp: number; platform: string[]; worldId: string; kind: string; attachments: string[]; tdVersion?: string; } export interface TicksReport { dateStr: string; sessionEvents: any; platform: string[]; worldId: string; jsUrl: string; } export interface RecordedEvents { dateStr:string; // YYYY.MM.DD sessionEvents:any; // { (tickName: number)* } chunkId:number; } export module Ticker { var logMsgs:TickEvent[] = []; var logSz = 200; var logIdx = -1; var thresholdTime = -1 var dateStr = "" var sessionEvents:any = {} var disabled = false; var delay = 10; // initial, 10s var chunkId = 1; var maxDelay = 1200; // 1200s var initialized = false; export var mainJsName = "unknown"; export var fillEditorInfoBugReport: (b: BugReport) => void; export var fillEditorInfoTicksReport: (b: TicksReport) => void; function setDate():void { var now = new Date() dateStr = Util.fmt("{0}.{1}.{2}", now.getFullYear(), now.getMonth() + 1, now.getDate()) if (delay > maxDelay) delay = maxDelay; thresholdTime = now.getTime() + delay*1000; } export function disable() { disabled = true } export function init() { var d = window.localStorage["ticksDelay"] * 1; if (d) delay = d; setDate(); var archived = window.localStorage["archivedEvents"] if (archived) { var prevEvents:RecordedEvents[]; try { prevEvents = JSON.parse(archived) } catch (e) { Util.log("bogus JSON in archivedEvents: " + archived) Util.check(false, "malformed JSON in archived events"); delete window.localStorage["archivedEvents"] } if (prevEvents) { prevEvents.forEach((e) => { chunkId = Math.max(e.chunkId, chunkId) }); chunkId++; var last = prevEvents.peek() if (!!last && last.dateStr == dateStr) { sessionEvents = last.sessionEvents prevEvents.pop() window.localStorage["archivedEvents"] = JSON.stringify(prevEvents) } sendOutEvents(prevEvents); } } initialized = true; } export function saveCurrent(sendCurrent = false) { var prevEvents:RecordedEvents[] = [] var archived = window.localStorage["archivedEvents"] if (archived) prevEvents = JSON.parse(archived) prevEvents.push({ sessionEvents: sessionEvents, dateStr: dateStr, chunkId: chunkId++ }) prevEvents = prevEvents.filter((e) => !!e.dateStr) if (prevEvents.length > 30) prevEvents = prevEvents.slice(prevEvents.length - 30) var newVal = JSON.stringify(prevEvents) if (newVal.length > 50000) newVal = "[]" // drop it if it's getting big window.localStorage["archivedEvents"] = newVal if (sendCurrent && Cloud.getAccessToken() && Cloud.isOnline()) (sendOutEvents)(prevEvents); // TSBUG: stack overflow } function checkDate():void { if (Util.now() > thresholdTime) { setDate() saveCurrent(true) RT.Perf.saveCurrent(true); if (delay < maxDelay) { delay *= 1.1; window.localStorage["ticksDelay"] = delay + ""; } sessionEvents = {} } } function sendOutEvents(events:RecordedEvents[]) { if (events.length == 0) return; var currId = events[0].chunkId var req = { dateStr: events[0].dateStr, sessionEvents: events[0].sessionEvents, platform: Browser.platformCaps, worldId: "", jsUrl: mainJsName } if (fillEditorInfoTicksReport) fillEditorInfoTicksReport(req); Cloud.postTicksAsync(req).done(() => { var events = window.localStorage["archivedEvents"] if (events) { var newEvents:RecordedEvents[] = JSON.parse(events); window.localStorage["archivedEvents"] = JSON.stringify(newEvents.filter((t) => t.chunkId != currId)) sendOutEvents(newEvents) } }, e => { }) // ignore error; will try again later } export function tickName(t:Ticks):string { return enumToString(Ticks, t); } export function tickN(t:Ticks, v:number) { tick(t + Util.boundTo(0, v, 5)) } export function dbg(f:string, ...args:any[]) { var msg = Util.fmt_va(f, args); tick(Ticks.dbgEvent, msg); } export function localStorageState() { if (!window || !window.localStorage) return ""; var l = window.localStorage; var r = ""; for (var i = 0; i < l.length; ++i) { r += l.key(i) + " -> " + l[l.key(i)].length + ", "; } return r; } export function mkBugReport(err:any, ctx = "") { var r:BugReport = { exceptionConstructor: "(unknown)", exceptionMessage: "(unknown)", context: ctx, currentUrl: "", worldId: "", kind: "", scriptId: "", stackTrace: "", sourceURL: "", line: -1, eventTrace: "", userAgent: "", resolution: "", jsUrl: mainJsName, timestamp: Util.now(), platform: [], attachments: [], tdVersion: Cloud.config.tdVersion || "", } if (fillEditorInfoBugReport) fillEditorInfoBugReport(r); if (Array.isArray(err.bugAttachments)) r.attachments.pushRange(err.bugAttachments) try { var isDatabaseError = Util.isError(err, e => e.isDatabaseError); var isNetworkError = Util.isError(err, e => e.isNetworkError); r.kind = isDatabaseError ? "databaseError" : isNetworkError ? "networkError" : ""; if (!err) r.exceptionMessage = "(null)"; else if (err.message) { r.exceptionMessage = err.message + ""; if (err.stack) r.stackTrace = err.stack + ""; else if (err.sourceURL) // Safari doesn't give full stack trace, just top-level method r.stackTrace = "@" + err.sourceURL + ":" + err.line; } else if (Array.isArray(err)) { r.exceptionMessage = err.join("\n"); } else { r.exceptionMessage = err + ""; } if (err && err.name && err.name != "Error") r.exceptionConstructor = err.name; else r.exceptionConstructor = r.exceptionMessage.substr(0, 40); if (isDatabaseError) r.exceptionConstructor = "DB " + r.exceptionConstructor; if (/autotest-/.test(ctx)) r.exceptionConstructor = "TEST " + r.exceptionConstructor; if (err.wabCrashInfo) { r.stackTrace = err.wabCrashInfo + "\nJavaScript:\n" + r.stackTrace; r.exceptionConstructor = "WAB " + r.exceptionConstructor; } if (r.exceptionMessage && /\[object /.test(r.exceptionMessage)) { var msg = r.exceptionMessage; var errorInfo = Util.getErrorInfo(err); if (errorInfo) msg += " " + errorInfo; else Object.keys(err).forEach((k) => { if (msg.length < 2000) { msg += " " + k + ": '" + err[k] + "'"; } }); r.exceptionMessage = msg; } if (/QUOTA/.test(r.exceptionMessage)) r.exceptionMessage += " " + localStorageState(); Ticker.dbg("CRASH REPORT " + r.exceptionMessage); // in case there is another crash report later if (err.sourceURL) r.sourceURL = err.sourceURL; if (err.line) r.line = err.line; } catch (e) { debugger; } try { r.eventTrace = getRecentEvents().map((e) => { var s = 1000000000 + (r.timestamp - e.timestamp) + ""; s = s.slice(-9); return s.slice(0, 6) + "." + s.slice(6, 9) + ": " + tickName(e.event) + (e.arg ? "|" + e.arg : ""); }).join("\n") if (!r.eventTrace) r.eventTrace = Util.getLogMsgs().map(m => m.elapsed + ": " + m.msg).join("\n"); } catch (e) { debugger; } if ((window).tdAppInsights) try { (window).tdAppInsights.trackException(err, r); } catch (e) { } return r; } export function bugReportToString(b:BugReport) { var e = (s:string) => s.replace(/\r?\n/, " "); return "Error: " + e(b.exceptionMessage) + "\n" + "URL: " + e(b.currentUrl) + " (script: " + e(b.scriptId) + ", context: " + e(b.context) + ")\n" + "JS-URL: " + e(b.jsUrl) + "\n" + "Platform: " + e(Browser.platformCaps.join(", ")) + "\n" + "UserAgent: " + e(b.userAgent) + " (resolution: " + e(b.resolution) + ")\n" + "Date: " + new Date(b.timestamp).toString() + "\n" + "\n" + "RecentEvents:\n" + b.eventTrace + "\n\n" + "StackTrace:\n" + b.stackTrace + "\n\n"; } export function bugReportForHash(b:BugReport) { return b.exceptionMessage + " " + b.stackTrace; } export function rawTick(tn:string) { if (!initialized || disabled) return; checkDate(); tn = tn.replace(/[^a-zA-Z_]/g, "_") Util.log("TICK: " + tn) if (sessionEvents[tn]) sessionEvents[tn]++; else sessionEvents[tn] = 1; } function tickBase(t: Ticks, sep: string, arg?: string) { if (t == Ticks.noEvent || !t) return; var tn = tickName(t) if (arg) tn += sep + arg; if (t == Ticks.dbgLogEvent) { t = Ticks.dbgEvent; } else if (t == Ticks.dbgEvent) Util.log("DBG: " + tn); else Util.log("TICK: " + tn); if (!initialized || disabled) return; checkDate(); var m = { timestamp: Util.now(), event: t, arg: arg } if (logIdx >= 0) { logMsgs[logIdx++] = m; if (logIdx >= logSz) logIdx = 0; } else { logMsgs.push(m); if (logMsgs.length >= logSz) logIdx = 0; } // this one we only wanted logged, not counted if (t != Ticks.dbgEvent) { if (sessionEvents[tn]) sessionEvents[tn]++; else sessionEvents[tn] = 1; if ((window).tdAppInsights) try { (window).tdAppInsights.trackEvent(tn); } catch (e) { } } } export function tickArg(t: Ticks, arg?: string) { return tickBase(t, "$", arg); } export function tick(t:Ticks, arg?:string) { return tickBase(t, "|", arg); } export function getRecentEvents() { if (logIdx >= 0) return logMsgs.slice(logIdx).concat(logMsgs.slice(0, logIdx)); else return logMsgs.slice(0); } } export function tick(t: Ticks, arg?: string) { Ticker.tick(t, arg) } export function tickArg(t: Ticks, arg?: string) { Ticker.tickArg(t, arg) } export function tickN(t:Ticks, v:number) { Ticker.tickN(t, v) } }