mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
Add proper game analytics
This commit is contained in:
parent
2411054ad2
commit
241b4b42d9
34
gulp/html.js
34
gulp/html.js
@ -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…
Reference in New Issue
Block a user