From d9bf30765f2d6439049273b212303c8bf465cad7 Mon Sep 17 00:00:00 2001 From: Slava Bacherikov Date: Thu, 8 Feb 2018 01:59:36 +0200 Subject: [PATCH] Add google analytics support Add code to track performance metrics and events (password calculation). --- ci/build.sh | 9 +++++++++ externs/googleAnalytics.js | 2 ++ externs/performance.js | 17 ++++++++++++++++ src/global.d.ts | 12 +++++++++++ src/googleAnalytics.ts | 20 ++++++++++++++++++ src/keygen.ts | 11 +++++++--- src/performancePolyfill.spec.ts | 7 +++++++ src/performancePolyfill.ts | 20 ++++++++++++++++++ src/ui.ts | 36 +++++++++++++++++++++++++++------ src/utils.ts | 2 +- tsconfig.json | 1 + webpack.config.js | 13 +++++++++++- 12 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 externs/googleAnalytics.js create mode 100644 externs/performance.js create mode 100644 src/global.d.ts create mode 100644 src/googleAnalytics.ts create mode 100644 src/performancePolyfill.spec.ts create mode 100644 src/performancePolyfill.ts diff --git a/ci/build.sh b/ci/build.sh index 4339483..947e3c3 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,6 +1,8 @@ #!/bin/sh set -e CLOSURE_COMPILER_URL="https://dl.google.com/closure-compiler/compiler-latest.zip" +PROD_GOOGLE_ANALYTICS="UA-112154345-1" +STAGE_GOOGLE_ANALYTCIS="UA-112154345-2" install_closure() { wget $CLOSURE_COMPILER_URL -O /tmp/closure.zip @@ -11,6 +13,13 @@ install_closure() { } build_package() { + if [ -n "$TRAVIS_TAG" ]; then + export GOOGLE_ANALYTICS_TAG="$PROD_GOOGLE_ANALYTICS" + echo "Using prod google analytics tag" + else + export GOOGLE_ANALYTICS_TAG="$STAGE_GOOGLE_ANALYTCIS" + echo "Using stage google analytics tag" + fi PRODUCTION=1 npm run webpack } diff --git a/externs/googleAnalytics.js b/externs/googleAnalytics.js new file mode 100644 index 0000000..07353ef --- /dev/null +++ b/externs/googleAnalytics.js @@ -0,0 +1,2 @@ + +var dataLayer; diff --git a/externs/performance.js b/externs/performance.js new file mode 100644 index 0000000..cd633fb --- /dev/null +++ b/externs/performance.js @@ -0,0 +1,17 @@ +/** + * @return {number} + * @nosideeffects + */ +Performance.prototype.mozNow = function() {}; + +/** + * @return {number} + * @nosideeffects + */ +Performance.prototype.oNow = function() {}; + +/** + * @return {number} + * @nosideeffects + */ +Performance.prototype.msNow = function() {}; diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..3c9589a --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,12 @@ +declare const GOOGLE_ANALYTICS_TAG: string | undefined; + +interface Window { + dataLayer: any[]; +} + +interface Performance { + webkitNow(): number; + mozNow(): number; + oNow(): number; + msNow(): number; +} diff --git a/src/googleAnalytics.ts b/src/googleAnalytics.ts new file mode 100644 index 0000000..b39ea4e --- /dev/null +++ b/src/googleAnalytics.ts @@ -0,0 +1,20 @@ +/* tslint:disable:no-empty */ +/* tslint:disable:only-arrow-functions */ +export let gtag: (...args: any[]) => void = () => {}; + +if (GOOGLE_ANALYTICS_TAG) { + let gaElem = document.createElement("script"); + gaElem.async = true; + let tag = GOOGLE_ANALYTICS_TAG; + gaElem.src = `https://www.googletagmanager.com/gtag/js?id=${tag}`; + let firstScript = document.getElementsByTagName("script")[0] as HTMLScriptElement; + (firstScript.parentNode as HTMLElement).insertBefore(gaElem, firstScript); + + window.dataLayer = window.dataLayer || []; + gtag = function() { window.dataLayer.push(arguments); }; + gtag("js", new Date()); + gtag("config", GOOGLE_ANALYTICS_TAG, { + "custom_map": {"dimension1": "siteVersion"}, + "transport_type": "beacon" + }); +} diff --git a/src/keygen.ts b/src/keygen.ts index 6d6c726..8b19ff9 100644 --- a/src/keygen.ts +++ b/src/keygen.ts @@ -2,6 +2,7 @@ import { dellHddSolver, dellSolver, hddOldSolver } from "./dell"; import { fsi20DecNewSolver, fsi20DecOldSolver, fsiHexSolver } from "./fsi"; import { hpMiniSolver } from "./hpmini"; import { insydeSolver } from "./insyde"; +import { monotonicTime } from "./performancePolyfill"; import { phoenixFsiLSolver, phoenixFsiPSolver, phoenixFsiSolver, phoenixFsiSSolver, phoenixFsiXSolver, phoenixHPCompaqSolver, phoenixSolver @@ -10,7 +11,7 @@ import { samsungSolver } from "./samsung"; import { sonySolver } from "./sony"; import { Solver } from "./utils"; -export type KeygenResult = [Solver, string[]]; +export type KeygenResult = [Solver, string[], number]; export const solvers: Solver[] = [ sonySolver, @@ -35,6 +36,10 @@ export const solvers: Solver[] = [ // NOTE: In future this function can change or even be removed export function keygen(serial: string): KeygenResult[] { return solvers - .map((solver): KeygenResult => [solver, solver(serial)]) - .filter(([_, passwords]) => passwords !== undefined && passwords.length >= 1); + .map((solver): KeygenResult => { + let startTime = monotonicTime(); + let passwords = solver(serial); + let calcTime = monotonicTime() - startTime; + return [solver, passwords, calcTime]; + }).filter(([_, passwords]) => passwords !== undefined && passwords.length >= 1); } diff --git a/src/performancePolyfill.spec.ts b/src/performancePolyfill.spec.ts new file mode 100644 index 0000000..e83336f --- /dev/null +++ b/src/performancePolyfill.spec.ts @@ -0,0 +1,7 @@ +import { monotonicTime } from "./performancePolyfill"; + +describe("Check performance.now polyfill", () => { + it("monotonicTime should return number", () => { + expect(typeof monotonicTime()).toEqual("number"); + }); +}); diff --git a/src/performancePolyfill.ts b/src/performancePolyfill.ts new file mode 100644 index 0000000..808ffce --- /dev/null +++ b/src/performancePolyfill.ts @@ -0,0 +1,20 @@ + +function makeMonotonicTime(): () => number { + if (typeof performance !== "undefined" && performance) { + let nowFun = performance.now || + performance.webkitNow || + performance.mozNow || + performance.oNow || + performance.msNow; + + if (nowFun) { + return nowFun.bind(performance); + } else { + return Date.now; + } + } else { + return Date.now; + } +} + +export const monotonicTime = makeMonotonicTime(); diff --git a/src/ui.ts b/src/ui.ts index 21cf8aa..a914a36 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,6 +1,5 @@ -import { keygen, KeygenResult } from "./keygen"; - -/// +import { gtag } from "./googleAnalytics"; +import { keygen, KeygenResult, solvers } from "./keygen"; let form = document.getElementById("form_id") as HTMLFormElement; let serialInput = document.getElementById("serial_id") as HTMLInputElement; @@ -11,10 +10,10 @@ let answerElement = document.getElementById("answer") as HTMLDivElement; function renderResult(results: KeygenResult[]): void { let table = document.createElement("table"); table.classList.add("answer_table"); - results.forEach(([solver, passwords]) => { + results.forEach(([solver, passwords, _]) => { let trElem = document.createElement("tr"); let tdNameElem = document.createElement("td"); - tdNameElem.appendChild(document.createTextNode(solver.biosName)); + tdNameElem.appendChild(document.createTextNode(solver.description)); trElem.appendChild(tdNameElem); let tdElem = document.createElement("td"); passwords.forEach((pwd, index) => { @@ -29,6 +28,31 @@ function renderResult(results: KeygenResult[]): void { answerElement.appendChild(table); } +function trackResult(serial: string): KeygenResult[] { + let result = keygen(serial); + if (GOOGLE_ANALYTICS_TAG) { + let eventParams: {[key: string]: boolean} = {}; + solvers.forEach((solver) => { + eventParams[solver.biosName] = false; + }); + + result.forEach(([solver, _, time]) => { + eventParams[solver.biosName] = true; + + // send calculation timing report + gtag("event", "timing_complete", { + "name": "bios_keygen", + "value": Math.round(time), + "event_category": solver.biosName + }); + }); + + // send event about calculation + gtag("event", "bios_keygen", eventParams); + } + return result; +} + form.addEventListener("submit", (event) => { if (event.preventDefault) { event.preventDefault(); @@ -36,7 +60,7 @@ form.addEventListener("submit", (event) => { event.returnValue = false; } answerElement.innerHTML = ""; // remove old table - let results = keygen(serialInput.value); + let results = trackResult(serialInput.value); if (results.length > 0) { tryThis.style.display = ""; dellNote.style.display = ""; diff --git a/src/utils.ts b/src/utils.ts index af55564..60c67b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,7 +13,7 @@ export interface Solver { (code: string): string[]; readonly biosName: string; readonly examples?: string[]; - readonly description?: string; + readonly description: string; validator(code: string): boolean; cleaner(code: string): string; keygen(code: string): string[]; diff --git a/tsconfig.json b/tsconfig.json index 49a83f7..87a3f4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "lib": ["dom", "es5"], "target": "es3", "noImplicitAny": true, "noImplicitReturns": true, diff --git a/webpack.config.js b/webpack.config.js index 9f163d1..745ecf6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,12 +4,20 @@ const ClosureCompilerPlugin = require('webpack-closure-compiler'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); +const webpack = require('webpack'); function makeClosureCompiler() { + const externDir = path.join(__dirname, "externs"); + const externs = [ + path.join(externDir, "googleAnalytics.js"), + path.join(externDir, "performance.js") + ]; + var pluginOptions = { compiler: { compilation_level: "ADVANCED", - create_source_map: true + create_source_map: true, + externs: externs }, concurrency: os.cpus().length }; @@ -29,6 +37,9 @@ var plugins = [ to: 'assets/' } ]), + new webpack.DefinePlugin({ + GOOGLE_ANALYTICS_TAG: JSON.stringify(process.env.GOOGLE_ANALYTICS_TAG) + }), new HtmlWebpackPlugin({ minify: { collapseWhitespace: true,