non-user identifying analytics

This commit is contained in:
RandomityGuy 2023-10-02 01:48:42 +05:30
parent 3dbee00b0b
commit 177e7903b2
5 changed files with 221 additions and 3 deletions

147
src/Analytics.hx Normal file
View file

@ -0,0 +1,147 @@
package src;
import haxe.Json;
import src.Util;
import src.Settings;
import src.Http;
import src.Console;
typedef PayloadData = {
type:String,
payload:{
hostname:String, language:String, referrer:String, screen:String, title:String, url:String, website:String, name:String, ?data:Dynamic
}
};
// I'm sorry to add this
// Your data is private and anonymous and we don't track you at all, I promise!
// The analytics are stored in a self hosted Umami instance inside EU.
class Analytics {
static var umami = "https://analytics.randomityguy.me/api/send";
public static function trackSingle(eventName:String) {
var p = payload(eventName, null);
var json = Json.stringify(p);
Http.post(umami, json, (b) -> {
// Console.log("Analytics suceeded: " + b.toString());
}, (e) -> {
// Console.log("Analytics failed: " + e);
});
}
public static function trackLevelPlay(levelName:String, levelFile:String) {
var p = payload("level-play", {
name: levelName,
file: levelFile
});
var json = Json.stringify(p);
Http.post(umami, json, (b) -> {
// Console.log("Analytics suceeded: " + b.toString());
}, (e) -> {
// Console.log("Analytics failed: " + e);
});
}
public static function trackLevelScore(levelName:String, levelFile:String, time:Int, oobs:Int, respawns:Int, rewind:Bool) {
var p = payload("level-score", {
name: levelName,
file: levelFile,
time: time,
oobs: oobs,
respawns: respawns,
rewind: rewind
});
var json = Json.stringify(p);
Http.post(umami, json, (b) -> {
// Console.log("Analytics suceeded: " + b.toString());
}, (e) -> {
// Console.log("Analytics failed: " + e);
});
}
public static function trackLevelQuit(levelName:String, levelFile:String, time:Int, oobs:Int, respawns:Int, rewind:Bool) {
var p = payload("level-quit", {
name: levelName,
file: levelFile,
time: time,
oobs: oobs,
respawns: respawns,
rewind: rewind
});
var json = Json.stringify(p);
Http.post(umami, json, (b) -> {
// Console.log("Analytics suceeded: " + b.toString());
}, (e) -> {
// Console.log("Analytics failed: " + e);
});
}
public static function trackPlatformInfo() {
var p = payload("device-telemetry", {
platform: Util.getPlatform(),
screen: screen(),
});
var json = Json.stringify(p);
Http.post(umami, json, (b) -> {
// Console.log("Analytics suceeded: " + b.toString());
}, (e) -> {
// Console.log("Analytics failed: " + e);
});
}
static function payload(eventName:String, eventData:Dynamic):PayloadData {
var p:PayloadData = {
type: "event",
payload: {
hostname: hostname(),
language: language(),
referrer: referrer(),
screen: screen(),
title: "MBHaxe Platinum",
url: "/",
website: "e6da43f0-fc6a-49cb-a4a9-4b7e7745e538",
name: eventName
}
};
if (eventData == null)
return p;
p.payload.data = eventData;
return p;
}
static function hostname() {
#if js
return js.Browser.window.location.hostname;
#end
#if hl
return "marbleblast.randomityguy.me";
#end
}
static function language() {
#if js
return js.Browser.window.navigator.language;
#end
#if hl
return "en-us";
#end
}
static function referrer() {
#if js
return js.Browser.window.document.referrer;
#end
#if hl
return "";
#end
}
static function screen() {
#if js
return '${js.Browser.window.screen.width}x${js.Browser.window.screen.height}';
#end
#if hl
return '${Settings.optionsSettings.screenWidth}x${Settings.optionsSettings.screenHeight}';
#end
}
}

View file

@ -1,6 +1,7 @@
package src;
import src.Console;
import src.Util;
typedef HttpRequest = {
var url:String;
@ -8,6 +9,8 @@ typedef HttpRequest = {
var errCallback:String->Void;
var cancelled:Bool;
var fulfilled:Bool;
var post:Bool;
var postData:String;
};
class Http {
@ -41,7 +44,7 @@ class Http {
var http = new sys.Http(req.url);
http.onError = (e) -> {
trace('HTTP Request Failed: ' + req.url);
responses.add(() -> req.errCallback(e));
responses.add(() -> req.errCallback(e + ":" + http.responseBytes.toString()));
req.fulfilled = true;
};
http.onBytes = (b) -> {
@ -53,7 +56,12 @@ class Http {
hl.Gc.blocking(true); // Wtf is this shit
trace('HTTP Request: ' + req.url);
#end
http.request(false);
if (req.post) {
http.setHeader('User-Agent', 'MBHaxe/1.0 ${Util.getPlatform()}');
http.setHeader('Content-Type', "application/json"); // support json data only (for now)
http.setPostData(req.postData);
}
http.request(req.post);
#if !MACOS_BUNDLE
hl.Gc.blocking(false);
#end
@ -68,7 +76,9 @@ class Http {
callback: callback,
errCallback: errCallback,
cancelled: false,
fulfilled: false
fulfilled: false,
post: false,
postData: null,
};
#if sys
requests.add(req);
@ -80,6 +90,35 @@ class Http {
#end
}
// Returns HTTPRequest on sys, Int on js
public static function post(url:String, postData:String, callback:haxe.io.Bytes->Void, errCallback:String->Void) {
var req = {
url: url,
callback: callback,
errCallback: errCallback,
cancelled: false,
fulfilled: false,
post: true,
postData: postData
};
#if sys
requests.add(req);
return req;
#else
return js.Browser.window.setTimeout(() -> {
js.Browser.window.fetch(url,
{
method: "POST",
headers: {
"User-Agent": js.Browser.window.navigator.userAgent,
"Content-Type": "application/json",
},
body: postData
}).then(r -> r.arrayBuffer().then(b -> callback(haxe.io.Bytes.ofData(b))), e -> errCallback(e.toString()));
}, 75);
#end
}
public static function loop() {
#if sys
var resp = responses.pop(false);

View file

@ -27,6 +27,7 @@ import src.Settings;
import src.Console;
import src.Debug;
import src.Gamepad;
import src.Analytics;
import src.PreviewWorld;
@:publicFields
@ -173,6 +174,9 @@ class MarbleGame {
Window.getInstance().removeEventTarget(@:privateAccess Key.onEvent);
#end
Analytics.trackSingle("game-start");
Analytics.trackPlatformInfo();
}
public function update(dt:Float) {
@ -282,6 +286,9 @@ class MarbleGame {
var missionType = world.mission.type;
var isNotCustom = !world.mission.isClaMission && !world.mission.isCustom;
world.setCursorLock(false);
var stats = Settings.levelStatistics[world.mission.path];
Analytics.trackLevelQuit(world.mission.title, world.mission.path, Std.int(world.timeState.timeSinceLoad * 1000), stats.oobs, stats.respawns,
Settings.optionsSettings.rewindEnabled);
world.dispose();
world = null;
paused = false;
@ -310,6 +317,7 @@ class MarbleGame {
if (world != null) {
world.dispose();
}
Analytics.trackLevelPlay(mission.title, mission.path);
world = new MarbleWorld(scene, scene2d, mission, toRecord);
world.init();
}
@ -317,6 +325,7 @@ class MarbleGame {
public function watchMissionReplay(mission:Mission, replay:Replay) {
canvas.clearContent();
destroyPreviewWorld();
Analytics.trackSingle("replay-watch");
world = new MarbleWorld(scene, scene2d, mission);
world.replay = replay;
world.isWatching = true;

View file

@ -93,6 +93,7 @@ import modes.GameMode;
import modes.NullMode;
import modes.GameMode.GameModeFactory;
import src.Renderer;
import src.Analytics;
class MarbleWorld extends Scheduler {
public var collisionWorld:CollisionWorld;
@ -1418,6 +1419,15 @@ class MarbleWorld extends Scheduler {
this.finishYaw = this.marble.camera.CameraYaw;
this.finishPitch = this.marble.camera.CameraPitch;
displayAlert("Congratulations! You've finished!");
if (!Settings.levelStatistics.exists(mission.path)) {
Settings.levelStatistics.set(mission.path, {
oobs: 0,
respawns: 0,
totalTime: 0,
});
}
Analytics.trackLevelScore(mission.title, mission.path, Std.int(finishTime.gameplayClock * 1000), Settings.levelStatistics[mission.path].oobs,
Settings.levelStatistics[mission.path].respawns, Settings.optionsSettings.rewindEnabled);
if (!this.isWatching) {
var myScore = {
name: "Player",

View file

@ -445,4 +445,17 @@ class Util {
return totBytes.getBytes().toString();
}
public static function getPlatform() {
#if js
return js.Browser.navigator.platform;
#end
#if hl
#if MACOS_BUNDLE
return "MacOS";
#else
return "Windows";
#end
#end
}
}