Add proper game analytics

pull/33/head
tobspr 4 years ago
parent 2411054ad2
commit 241b4b42d9

@ -13,15 +13,7 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) {
const commitHash = buildUtils.getRevision();
async function buildHtml(
apiUrl,
{
analytics = false,
standalone = false,
app = false,
integrity = true,
enableCachebust = true,
gameAnalyticsKey = null,
gameAnalyticsSecret = null,
}
{ analytics = false, standalone = false, app = false, integrity = true, enableCachebust = true }
) {
function cachebust(url) {
if (enableCachebust) {
@ -87,21 +79,6 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) {
// document.head.appendChild(logrocketInit);
}
if (gameAnalyticsKey && gameAnalyticsSecret) {
const gaLoader = document.createElement("script");
gaLoader.textContent = `
window.GameAnalytics=window.GameAnalytics||function(){(GameAnalytics.q=GameAnalytics.q||[]).push(arguments)};
window.ga_comKey = "${gameAnalyticsKey}";
window.ga_comToken = "${gameAnalyticsSecret}";
`;
document.head.appendChild(gaLoader);
const gaScript = document.createElement("script");
gaScript.src = "https://download.gameanalytics.com/js/GameAnalytics-4.0.10.min.js";
gaScript.setAttribute("async", "");
document.head.appendChild(gaScript);
}
if (app) {
// Append cordova link
const cdv = document.createElement("script");
@ -305,27 +282,18 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) {
analytics: false,
integrity: false,
enableCachebust: false,
gameAnalyticsKey: "c8d77921633d5c32a7134e5d5cfcdf12",
// Not an actual "secret" since its built into the JS code
gameAnalyticsSecret: "6d23b40a70199bff0e7a7d8a073543772cf07097",
});
});
gulp.task("html.staging", () => {
return buildHtml("https://api-staging.shapez.io", {
analytics: true,
gameAnalyticsKey: "903fa0dd2d2e23b07e66ea96ddc4c10c",
// Not an actual "secret" since its built into the JS code
gameAnalyticsSecret: "9417fc391d7142b9d73a3861ba6046cafa9df6cb",
});
});
gulp.task("html.prod", () => {
return buildHtml("https://api.shapez.io", {
analytics: true,
gameAnalyticsKey: "16c7f9d352e40c92f6a750fc1a4f0443",
// Not an actual "secret" since its built into the JS code
gameAnalyticsSecret: "4202d7adf154c325ff91731e8be6912e6c0d10e5",
});
});

@ -27,7 +27,7 @@ import { AnalyticsInterface } from "./platform/analytics";
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
import { Loader } from "./core/loader";
import { GameAnalyticsInterface } from "./platform/game_analytics";
import { GameAnalyticsDotCom } from "./platform/browser/game_analytics";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
const logger = createLogger("application");
@ -121,7 +121,7 @@ export class Application {
this.sound = new SoundImplBrowser(this);
this.platformWrapper = new PlatformWrapperImplBrowser(this);
this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new GameAnalyticsDotCom(this);
this.gameAnalytics = new ShapezGameAnalytics(this);
}
/**

@ -66,7 +66,7 @@ export const globalConfig = {
debug: {
/* dev:start */
fastGameEnter: false,
fastGameEnter: true,
noArtificialDelays: true,
disableSavegameWrite: false,
showEntityBounds: false,
@ -90,6 +90,9 @@ export const globalConfig = {
// Savegame salt
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
// Analytics key
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
},
};

@ -1,39 +1,194 @@
import { GameAnalyticsInterface } from "../game_analytics";
import { createLogger } from "../../core/logging";
import { ShapeDefinition } from "../../game/shape_definition";
import { gameCreationAction } from "../../states/ingame";
import { Savegame } from "../../savegame/savegame";
import { FILE_NOT_FOUND } from "../storage";
import { globalConfig } from "../../core/config";
import { InGameState } from "../../states/ingame";
import { GameRoot } from "../../game/root";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
const logger = createLogger("ga_com");
const logger = createLogger("game_analytics");
const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io";
const analyticsLocalFile = "analytics_token.bin";
export class GameAnalyticsDotCom extends GameAnalyticsInterface {
export class ShapezGameAnalytics extends GameAnalyticsInterface {
/**
* @returns {Promise<void>}
*/
initialize() {
try {
const ga = window.gameanalytics.GameAnalytics;
ga.configureBuild(G_APP_ENVIRONMENT + "@" + G_BUILD_VERSION + "@" + G_BUILD_COMMIT_HASH);
this.syncKey = null;
ga.setEnabledInfoLog(G_IS_DEV);
ga.setEnabledVerboseLog(G_IS_DEV);
setInterval(() => this.sendTimePoints(), 30 * 1000);
// @ts-ignore
ga.initialize(window.ga_comKey, window.ga_comToken);
// Retrieve sync key from player
return this.app.storage.readFileAsync(analyticsLocalFile).then(
syncKey => {
this.syncKey = syncKey;
logger.log("Player sync key read:", this.syncKey);
},
error => {
// File was not found, retrieve new key
if (error === FILE_NOT_FOUND) {
logger.log("Retrieving new player key");
// start new session
ga.startSession();
} catch (ex) {
logger.warn("ga_com init error:", ex);
// Perform call to get a new key from the API
this.sendToApi("/v1/register", {
environment: G_APP_ENVIRONMENT,
})
.then(res => {
// Try to read and parse the key from the api
if (res.key && typeof res.key === "string" && res.key.length === 40) {
this.syncKey = res.key;
logger.log("Key retrieved:", this.syncKey);
this.app.storage.writeFileAsync(analyticsLocalFile, res.key);
} else {
throw new Error("Bad response from analytics server: " + res);
}
})
.catch(err => {
logger.error("Failed to register on analytics api:", err);
});
} else {
logger.error("Failed to read ga key:", error);
}
return;
}
);
}
/**
* Sends a request to the api
* @param {string} endpoint Endpoint without base url
* @param {object} data payload
* @returns {Promise<any>}
*/
sendToApi(endpoint, data) {
return Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000);
}),
fetch(analyticsUrl + endpoint, {
method: "POST",
mode: "cors",
cache: "no-cache",
referrer: "no-referrer",
credentials: "omit",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"x-api-key": globalConfig.info.analyticsApiKey,
},
body: JSON.stringify(data),
})
.then(res => {
if (!res.ok || res.status !== 200) {
throw new Error("Fetch error: Bad status " + res.status);
}
return res;
})
.then(res => res.json()),
]);
}
/**
* Sends a game event to the analytics
* @param {string} category
* @param {string} value
*/
sendGameEvent(category, value) {
if (!this.syncKey) {
logger.warn("Can not send event due to missing sync key");
return;
}
return Promise.resolve();
const gameState = this.app.stateMgr.currentState;
if (!(gameState instanceof InGameState)) {
logger.warn("Trying to send analytics event outside of ingame state");
return;
}
const savegame = gameState.savegame;
if (!savegame) {
logger.warn("Ingame state has empty savegame");
return;
}
const savegameId = savegame.internalId;
const root = gameState.core.root;
if (!root) {
logger.warn("Root is not initialized");
return;
}
logger.log("Sending event", category, value);
this.sendToApi("/v1/game-event", {
playerKey: this.syncKey,
gameKey: savegameId,
ingameTime: root.time.now(),
category,
value,
gameDump: this.generateGameDump(root),
});
}
sendTimePoints() {
const gameState = this.app.stateMgr.currentState;
if (gameState instanceof InGameState) {
logger.log("Syncing analytics");
this.sendGameEvent("sync", "");
}
}
/**
* Generates a game dump
* @param {GameRoot} root
*/
generateGameDump(root) {
let staticEntities = [];
const entities = root.entityMgr.getAllWithComponent(StaticMapEntityComponent);
for (let i = 0; i < entities.length; ++i) {
const entity = entities[i];
const staticComp = entity.components.StaticMapEntity;
const payload = {};
payload.origin = staticComp.origin;
payload.tileSize = staticComp.tileSize;
payload.rotation = staticComp.rotation;
if (entity.components.Belt) {
payload.type = "belt";
} else if (entity.components.UndergroundBelt) {
payload.type = "tunnel";
} else if (entity.components.ItemProcessor) {
payload.type = entity.components.ItemProcessor.type;
} else if (entity.components.Miner) {
payload.type = "extractor";
} else {
logger.warn("Unkown entity type", entity);
}
staticEntities.push(payload);
}
return {
storedShapes: root.hubGoals.storedShapes,
gainedRewards: root.hubGoals.gainedRewards,
upgradeLevels: root.hubGoals.upgradeLevels,
staticEntities,
};
}
/**
* @param {ShapeDefinition} definition
*/
handleShapeDelivered(definition) {
const hash = definition.getHash();
logger.log("Deliver:", hash);
handleShapeDelivered(definition) {}
/**
*/
handleGameStarted() {
this.sendGameEvent("game_start", "");
}
/**
@ -42,13 +197,7 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface {
*/
handleLevelCompleted(level) {
logger.log("Complete level", level);
try {
const gaD = window.gameanalytics;
const ga = gaD.GameAnalytics;
ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "story", "" + level);
} catch (ex) {
logger.error("ga_com lvl complete error:", ex);
}
this.sendGameEvent("level_complete", "" + level);
}
/**
@ -58,12 +207,6 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface {
*/
handleUpgradeUnlocked(id, level) {
logger.log("Unlock upgrade", id, level);
try {
const gaD = window.gameanalytics;
const ga = gaD.GameAnalytics;
ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "upgrade", id, "" + level);
} catch (ex) {
logger.error("ga_com upgrade unlock error:", ex);
}
this.sendGameEvent("upgrade_unlock", id + "@" + level);
}
}

@ -1,6 +1,7 @@
/* typehints:start */
import { Application } from "../application";
import { ShapeDefinition } from "../game/shape_definition";
import { Savegame } from "../savegame/savegame";
/* typehints:end */
export class GameAnalyticsInterface {
@ -18,6 +19,11 @@ export class GameAnalyticsInterface {
return Promise.reject();
}
/**
* Handles a new game which was started
*/
handleGameStarted() {}
/**
* @param {ShapeDefinition} definition
*/

@ -186,14 +186,9 @@ export class SavegameManager extends ReadWriteProxy {
* Helper method to generate a new internal savegame id
*/
generateInternalId() {
const timestamp = ("" + Math_floor(Date.now() / 1000.0 - 1565641619)).padStart(10, "0");
return (
timestamp +
"." +
Rusha.createHash()
.update(Date.now() + "/" + Math.random())
.digest("hex")
);
return Rusha.createHash()
.update(Date.now() + "/" + Math.random())
.digest("hex");
}
// End

@ -198,8 +198,10 @@ export class InGameState extends GameState {
this.core.initializeRoot(this, this.savegame);
if (this.savegame.hasGameDump()) {
this.app.gameAnalytics.handleGameStarted(this.savegame);
this.stage4bResumeGame();
} else {
this.app.gameAnalytics.handleGameStarted(this.savegame);
this.stage4aInitEmptyGame();
}
}

Loading…
Cancel
Save