diff --git a/gulp/html.js b/gulp/html.js
index b8af0e53..d9889fe1 100644
--- a/gulp/html.js
+++ b/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",
});
});
diff --git a/src/js/application.js b/src/js/application.js
index 75d47ab0..0690ec55 100644
--- a/src/js/application.js
+++ b/src/js/application.js
@@ -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);
}
/**
diff --git a/src/js/core/config.js b/src/js/core/config.js
index 175ca319..5fe775b4 100644
--- a/src/js/core/config.js
+++ b/src/js/core/config.js
@@ -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",
},
};
diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js
index 1c06a602..bb814266 100644
--- a/src/js/platform/browser/game_analytics.js
+++ b/src/js/platform/browser/game_analytics.js
@@ -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}
*/
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}
+ */
+ 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);
}
}
diff --git a/src/js/platform/game_analytics.js b/src/js/platform/game_analytics.js
index 5b70e565..c3e2fa64 100644
--- a/src/js/platform/game_analytics.js
+++ b/src/js/platform/game_analytics.js
@@ -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
*/
diff --git a/src/js/savegame/savegame_manager.js b/src/js/savegame/savegame_manager.js
index 69a275bb..60f74989 100644
--- a/src/js/savegame/savegame_manager.js
+++ b/src/js/savegame/savegame_manager.js
@@ -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
diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js
index 6a400a14..eda9a9fb 100644
--- a/src/js/states/ingame.js
+++ b/src/js/states/ingame.js
@@ -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();
}
}