mirror of
				https://github.com/tobspr/shapez.io.git
				synced 2025-06-13 13:04:03 +00:00 
			
		
		
		
	Add proper game analytics
This commit is contained in:
		
							parent
							
								
									b5888369c0
								
							
						
					
					
						commit
						01f96e534e
					
				
							
								
								
									
										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