mirror of
https://github.com/RandomityGuy/MBHaxe.git
synced 2026-01-17 04:32:10 +00:00
non-user identifying analytics
This commit is contained in:
parent
52a3b38e9d
commit
42bcc3d960
5 changed files with 319 additions and 0 deletions
147
src/Analytics.hx
Normal file
147
src/Analytics.hx
Normal 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
|
||||
}
|
||||
}
|
||||
137
src/Http.hx
Normal file
137
src/Http.hx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package src;
|
||||
|
||||
import src.Console;
|
||||
import src.Util;
|
||||
|
||||
typedef HttpRequest = {
|
||||
var url:String;
|
||||
var callback:haxe.io.Bytes->Void;
|
||||
var errCallback:String->Void;
|
||||
var cancelled:Bool;
|
||||
var fulfilled:Bool;
|
||||
var post:Bool;
|
||||
var postData:String;
|
||||
};
|
||||
|
||||
class Http {
|
||||
#if sys
|
||||
static var threadPool:sys.thread.FixedThreadPool;
|
||||
static var requests:sys.thread.Deque<HttpRequest> = new sys.thread.Deque<HttpRequest>();
|
||||
static var responses:sys.thread.Deque<() -> Void> = new sys.thread.Deque<() -> Void>();
|
||||
static var cancellationMutex:sys.thread.Mutex = new sys.thread.Mutex();
|
||||
#end
|
||||
|
||||
public static function init() {
|
||||
#if sys
|
||||
threadPool = new sys.thread.FixedThreadPool(4);
|
||||
threadPool.run(() -> threadLoop());
|
||||
threadPool.run(() -> threadLoop());
|
||||
threadPool.run(() -> threadLoop());
|
||||
threadPool.run(() -> threadLoop());
|
||||
#end
|
||||
}
|
||||
|
||||
#if sys
|
||||
static function threadLoop() {
|
||||
while (true) {
|
||||
var req = requests.pop(true);
|
||||
cancellationMutex.acquire();
|
||||
if (req.cancelled) {
|
||||
cancellationMutex.release();
|
||||
continue;
|
||||
}
|
||||
cancellationMutex.release();
|
||||
var http = new sys.Http(req.url);
|
||||
http.onError = (e) -> {
|
||||
responses.add(() -> req.errCallback(e + ":" + http.responseBytes.toString()));
|
||||
req.fulfilled = true;
|
||||
};
|
||||
http.onBytes = (b) -> {
|
||||
responses.add(() -> req.callback(b));
|
||||
req.fulfilled = true;
|
||||
};
|
||||
hl.Gc.enable(false);
|
||||
hl.Gc.blocking(true); // Wtf is this shit
|
||||
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);
|
||||
hl.Gc.blocking(false);
|
||||
hl.Gc.enable(true);
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
// Returns HTTPRequest on sys, Int on js
|
||||
public static function get(url:String, callback:haxe.io.Bytes->Void, errCallback:String->Void) {
|
||||
var req = {
|
||||
url: url,
|
||||
callback: callback,
|
||||
errCallback: errCallback,
|
||||
cancelled: false,
|
||||
fulfilled: false,
|
||||
post: false,
|
||||
postData: null,
|
||||
};
|
||||
#if sys
|
||||
requests.add(req);
|
||||
return req;
|
||||
#else
|
||||
return js.Browser.window.setTimeout(() -> {
|
||||
js.Browser.window.fetch(url).then(r -> r.arrayBuffer().then(b -> callback(haxe.io.Bytes.ofData(b))), e -> errCallback(e.toString()));
|
||||
}, 75);
|
||||
#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);
|
||||
if (resp != null) {
|
||||
resp();
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
#if sys
|
||||
public static function cancel(req:HttpRequest) {
|
||||
cancellationMutex.acquire();
|
||||
req.cancelled = true;
|
||||
cancellationMutex.release();
|
||||
}
|
||||
#else
|
||||
public static function cancel(req:Int) {
|
||||
js.Browser.window.clearTimeout(req);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import gui.Canvas;
|
|||
import src.Util;
|
||||
import src.ProfilerUI;
|
||||
import src.Gamepad;
|
||||
import src.Analytics;
|
||||
|
||||
@:publicFields
|
||||
class MarbleGame {
|
||||
|
|
@ -155,6 +156,9 @@ class MarbleGame {
|
|||
|
||||
Window.getInstance().removeEventTarget(@:privateAccess Key.onEvent);
|
||||
#end
|
||||
|
||||
Analytics.trackSingle("game-start");
|
||||
Analytics.trackPlatformInfo();
|
||||
}
|
||||
|
||||
public function update(dt:Float) {
|
||||
|
|
@ -220,6 +224,9 @@ class MarbleGame {
|
|||
|
||||
public function quitMission() {
|
||||
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);
|
||||
paused = false;
|
||||
var pmg = new PlayMissionGui();
|
||||
PlayMissionGui.currentSelectionStatic = world.mission.index;
|
||||
|
|
@ -233,6 +240,10 @@ class MarbleGame {
|
|||
|
||||
public function playMission(mission:Mission) {
|
||||
canvas.clearContent();
|
||||
if (world != null) {
|
||||
world.dispose();
|
||||
}
|
||||
Analytics.trackLevelPlay(mission.title, mission.path);
|
||||
world = new MarbleWorld(scene, scene2d, mission, toRecord);
|
||||
toRecord = false;
|
||||
world.init();
|
||||
|
|
@ -240,6 +251,7 @@ class MarbleGame {
|
|||
|
||||
public function watchMissionReplay(mission:Mission, replay:Replay) {
|
||||
canvas.clearContent();
|
||||
Analytics.trackSingle("replay-watch");
|
||||
world = new MarbleWorld(scene, scene2d, mission);
|
||||
world.replay = replay;
|
||||
world.isWatching = true;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ import src.ProfilerUI;
|
|||
import src.ResourceLoaderWorker;
|
||||
import src.Gamepad;
|
||||
import src.ResourceLoader;
|
||||
import src.Analytics;
|
||||
|
||||
class MarbleWorld extends Scheduler {
|
||||
public var collisionWorld:CollisionWorld;
|
||||
|
|
@ -1335,6 +1336,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)
|
||||
this.schedule(this.timeState.currentAttemptTime + 2, () -> cast showFinishScreen());
|
||||
// Stop the ongoing sounds
|
||||
|
|
|
|||
13
src/Util.hx
13
src/Util.hx
|
|
@ -389,4 +389,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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue