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();
|
const commitHash = buildUtils.getRevision();
|
||||||
async function buildHtml(
|
async function buildHtml(
|
||||||
apiUrl,
|
apiUrl,
|
||||||
{
|
{ analytics = false, standalone = false, app = false, integrity = true, enableCachebust = true }
|
||||||
analytics = false,
|
|
||||||
standalone = false,
|
|
||||||
app = false,
|
|
||||||
integrity = true,
|
|
||||||
enableCachebust = true,
|
|
||||||
gameAnalyticsKey = null,
|
|
||||||
gameAnalyticsSecret = null,
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
function cachebust(url) {
|
function cachebust(url) {
|
||||||
if (enableCachebust) {
|
if (enableCachebust) {
|
||||||
@ -87,21 +79,6 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) {
|
|||||||
// document.head.appendChild(logrocketInit);
|
// 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) {
|
if (app) {
|
||||||
// Append cordova link
|
// Append cordova link
|
||||||
const cdv = document.createElement("script");
|
const cdv = document.createElement("script");
|
||||||
@ -305,27 +282,18 @@ function gulptasksHTML($, gulp, buildFolder, browserSync) {
|
|||||||
analytics: false,
|
analytics: false,
|
||||||
integrity: false,
|
integrity: false,
|
||||||
enableCachebust: false,
|
enableCachebust: false,
|
||||||
gameAnalyticsKey: "c8d77921633d5c32a7134e5d5cfcdf12",
|
|
||||||
// Not an actual "secret" since its built into the JS code
|
|
||||||
gameAnalyticsSecret: "6d23b40a70199bff0e7a7d8a073543772cf07097",
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("html.staging", () => {
|
gulp.task("html.staging", () => {
|
||||||
return buildHtml("https://api-staging.shapez.io", {
|
return buildHtml("https://api-staging.shapez.io", {
|
||||||
analytics: true,
|
analytics: true,
|
||||||
gameAnalyticsKey: "903fa0dd2d2e23b07e66ea96ddc4c10c",
|
|
||||||
// Not an actual "secret" since its built into the JS code
|
|
||||||
gameAnalyticsSecret: "9417fc391d7142b9d73a3861ba6046cafa9df6cb",
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("html.prod", () => {
|
gulp.task("html.prod", () => {
|
||||||
return buildHtml("https://api.shapez.io", {
|
return buildHtml("https://api.shapez.io", {
|
||||||
analytics: true,
|
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 { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
|
||||||
import { Loader } from "./core/loader";
|
import { Loader } from "./core/loader";
|
||||||
import { GameAnalyticsInterface } from "./platform/game_analytics";
|
import { GameAnalyticsInterface } from "./platform/game_analytics";
|
||||||
import { GameAnalyticsDotCom } from "./platform/browser/game_analytics";
|
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||||
|
|
||||||
const logger = createLogger("application");
|
const logger = createLogger("application");
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ export class Application {
|
|||||||
this.sound = new SoundImplBrowser(this);
|
this.sound = new SoundImplBrowser(this);
|
||||||
this.platformWrapper = new PlatformWrapperImplBrowser(this);
|
this.platformWrapper = new PlatformWrapperImplBrowser(this);
|
||||||
this.analytics = new GoogleAnalyticsImpl(this);
|
this.analytics = new GoogleAnalyticsImpl(this);
|
||||||
this.gameAnalytics = new GameAnalyticsDotCom(this);
|
this.gameAnalytics = new ShapezGameAnalytics(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,7 +66,7 @@ export const globalConfig = {
|
|||||||
|
|
||||||
debug: {
|
debug: {
|
||||||
/* dev:start */
|
/* dev:start */
|
||||||
fastGameEnter: false,
|
fastGameEnter: true,
|
||||||
noArtificialDelays: true,
|
noArtificialDelays: true,
|
||||||
disableSavegameWrite: false,
|
disableSavegameWrite: false,
|
||||||
showEntityBounds: false,
|
showEntityBounds: false,
|
||||||
@ -90,6 +90,9 @@ export const globalConfig = {
|
|||||||
|
|
||||||
// Savegame salt
|
// Savegame salt
|
||||||
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
||||||
|
|
||||||
|
// Analytics key
|
||||||
|
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,39 +1,194 @@
|
|||||||
import { GameAnalyticsInterface } from "../game_analytics";
|
import { GameAnalyticsInterface } from "../game_analytics";
|
||||||
import { createLogger } from "../../core/logging";
|
import { createLogger } from "../../core/logging";
|
||||||
import { ShapeDefinition } from "../../game/shape_definition";
|
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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
initialize() {
|
initialize() {
|
||||||
try {
|
this.syncKey = null;
|
||||||
const ga = window.gameanalytics.GameAnalytics;
|
|
||||||
ga.configureBuild(G_APP_ENVIRONMENT + "@" + G_BUILD_VERSION + "@" + G_BUILD_COMMIT_HASH);
|
|
||||||
|
|
||||||
ga.setEnabledInfoLog(G_IS_DEV);
|
setInterval(() => this.sendTimePoints(), 30 * 1000);
|
||||||
ga.setEnabledVerboseLog(G_IS_DEV);
|
|
||||||
|
|
||||||
// @ts-ignore
|
// Retrieve sync key from player
|
||||||
ga.initialize(window.ga_comKey, window.ga_comToken);
|
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
|
// Perform call to get a new key from the API
|
||||||
ga.startSession();
|
this.sendToApi("/v1/register", {
|
||||||
} catch (ex) {
|
environment: G_APP_ENVIRONMENT,
|
||||||
logger.warn("ga_com init error:", ex);
|
})
|
||||||
|
.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
|
* @param {ShapeDefinition} definition
|
||||||
*/
|
*/
|
||||||
handleShapeDelivered(definition) {
|
handleShapeDelivered(definition) {}
|
||||||
const hash = definition.getHash();
|
|
||||||
logger.log("Deliver:", hash);
|
/**
|
||||||
|
*/
|
||||||
|
handleGameStarted() {
|
||||||
|
this.sendGameEvent("game_start", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,13 +197,7 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface {
|
|||||||
*/
|
*/
|
||||||
handleLevelCompleted(level) {
|
handleLevelCompleted(level) {
|
||||||
logger.log("Complete level", level);
|
logger.log("Complete level", level);
|
||||||
try {
|
this.sendGameEvent("level_complete", "" + level);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,12 +207,6 @@ export class GameAnalyticsDotCom extends GameAnalyticsInterface {
|
|||||||
*/
|
*/
|
||||||
handleUpgradeUnlocked(id, level) {
|
handleUpgradeUnlocked(id, level) {
|
||||||
logger.log("Unlock upgrade", id, level);
|
logger.log("Unlock upgrade", id, level);
|
||||||
try {
|
this.sendGameEvent("upgrade_unlock", id + "@" + level);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* typehints:start */
|
/* typehints:start */
|
||||||
import { Application } from "../application";
|
import { Application } from "../application";
|
||||||
import { ShapeDefinition } from "../game/shape_definition";
|
import { ShapeDefinition } from "../game/shape_definition";
|
||||||
|
import { Savegame } from "../savegame/savegame";
|
||||||
/* typehints:end */
|
/* typehints:end */
|
||||||
|
|
||||||
export class GameAnalyticsInterface {
|
export class GameAnalyticsInterface {
|
||||||
@ -18,6 +19,11 @@ export class GameAnalyticsInterface {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a new game which was started
|
||||||
|
*/
|
||||||
|
handleGameStarted() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ShapeDefinition} definition
|
* @param {ShapeDefinition} definition
|
||||||
*/
|
*/
|
||||||
|
@ -186,14 +186,9 @@ export class SavegameManager extends ReadWriteProxy {
|
|||||||
* Helper method to generate a new internal savegame id
|
* Helper method to generate a new internal savegame id
|
||||||
*/
|
*/
|
||||||
generateInternalId() {
|
generateInternalId() {
|
||||||
const timestamp = ("" + Math_floor(Date.now() / 1000.0 - 1565641619)).padStart(10, "0");
|
return Rusha.createHash()
|
||||||
return (
|
.update(Date.now() + "/" + Math.random())
|
||||||
timestamp +
|
.digest("hex");
|
||||||
"." +
|
|
||||||
Rusha.createHash()
|
|
||||||
.update(Date.now() + "/" + Math.random())
|
|
||||||
.digest("hex")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// End
|
// End
|
||||||
|
@ -198,8 +198,10 @@ export class InGameState extends GameState {
|
|||||||
this.core.initializeRoot(this, this.savegame);
|
this.core.initializeRoot(this, this.savegame);
|
||||||
|
|
||||||
if (this.savegame.hasGameDump()) {
|
if (this.savegame.hasGameDump()) {
|
||||||
|
this.app.gameAnalytics.handleGameStarted(this.savegame);
|
||||||
this.stage4bResumeGame();
|
this.stage4bResumeGame();
|
||||||
} else {
|
} else {
|
||||||
|
this.app.gameAnalytics.handleGameStarted(this.savegame);
|
||||||
this.stage4aInitEmptyGame();
|
this.stage4aInitEmptyGame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user