Skip to content

Instantly share code, notes, and snippets.

@regner
Last active August 8, 2023 20:34
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save regner/592bb2cfca82f064ccd5322ea4c5deb8 to your computer and use it in GitHub Desktop.
Save regner/592bb2cfca82f064ccd5322ea4c5deb8 to your computer and use it in GitHub Desktop.
"""Very simple backend to recieve Studio Analytics telemetry from Unreal Engine."""
import os
from datetime import datetime, timezone
import sentry_sdk
from flask import Flask, Response, request
from sqlalchemy import create_engine, Column, Integer, TIMESTAMP, MetaData, Table
from sqlalchemy.dialects.postgresql import JSONB
from sentry_sdk.integrations.flask import FlaskIntegration
FLASK_ENVIRONMENT = os.environ.get("FLASK_ENVIRONMENT", "development")
if FLASK_ENVIRONMENT != "development":
sentry_sdk.init(
# Sentry DSN are OK to be public, they even have to be when using in web frontends
dsn="",
integrations=[
FlaskIntegration(),
],
traces_sample_rate=float(os.environ.get("SENTRY_TRACE_SAMPLE_RATE", "0.5")),
environment=FLASK_ENVIRONMENT,
)
app = Flask(__name__)
PG_CONNECTION_STRING = os.environ.get("PG_DB_CONNECTION_STRING", None)
db = create_engine(PG_CONNECTION_STRING)
engine = db.connect()
meta = MetaData(engine)
EVENTS = Table(
"events",
meta,
Column("id", Integer, primary_key=True),
Column("ingest_time", TIMESTAMP),
Column("event", JSONB),
)
meta.create_all()
@app.route("/health", methods=["GET"])
def health() -> Response:
"""Super simple healthcheck."""
connection = engine.connect()
connection.execute("SELECT 1")
connection.close()
return Response("{}", status=200, mimetype="application/json")
@app.route("/datarouter/api/v1/public/data", methods=["POST"])
def studio_analytics_v1() -> Response:
"""Super simple implementation of the Epic public data endpoint."""
args = request.args
content = request.get_json()
if content is not None:
ingest_time = datetime.now(tz=timezone.utc)
for event in content["Events"]:
event.update(args)
insert = EVENTS.insert().values(event=event, ingest_time=ingest_time)
connection = engine.connect()
connection.execute(insert)
connection.close()
return Response("{}", status=201, mimetype="application/json")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
FROM python:3.10-slim-buster
WORKDIR /app
COPY reiquirements.txt main.py
RUN pip install -r requirements.txt
CMD [ "python", "waitress_server" ]
flask==2.2.2
psycopg2==2.9.5
sentry-sdk[flask]==1.10.1
sqlalchemy==1.4.42
waitress==2.1.2
"""Simple Waitress wrapper for the backend in production."""
import logging
from waitress import serve
from analytics_backend import app
logger = logging.getLogger("waitress")
logger.setLevel(logging.DEBUG)
serve(
app,
host="0.0.0.0",
port=5000,
)
{
"AppID": "SomethingUniqueToCompany",
"UserID": "regnerblokandersen",
"P4Branch": "++project+main",
"FirstTime": true,
"SessionID": "{40FD32AB-44A1-BD58-05E0-37A2A12E5300}",
"AppVersion": "++project+main-CL-1111111",
"DateOffset": "+00:00:52.108",
"UploadType": "eteventstream",
"LoadingName": "InitializeEditor",
"ProjectName": "Project",
"ComputerName": "Workstation",
"AppEnvironment": "datacollector-binary",
"LoadingSeconds": 112.776953
}
{
"AppID": "SomethingUniqueToCompany",
"UserID": "regnerblokandersen",
"MapName": "Map",
"P4Branch": "++project+main",
"EventName": "Performance.Loading",
"FirstTime": true,
"SessionID": "{40FD32AB-44A1-BD58-05E0-37A2A12E5300}",
"AppVersion": "++project+main-CL-1111111",
"DateOffset": "+00:00:00.686",
"UploadType": "eteventstream",
"LoadingName": "LoadMap",
"ProjectName": "Project",
"ComputerName": "Workstation",
"AppEnvironment": "datacollector-binary",
"LoadingSeconds": 51.419099
}
{
"AppID": "SomethingUniqueToCompany",
"UserID": "regnerblokandersen",
"P4Branch": "++project+main",
"AssetPath": "World /Game/Maps/Map",
"AssetType": "World",
"EventName": "Performance.Loading",
"SessionID": "{4D8224C9-4978-A127-A6EC-2A87FC619BC3}",
"AppVersion": "++project+main-CL-1111111",
"DateOffset": "+00:00:00.096",
"UploadType": "eteventstream",
"LoadingName": "OpenAssetEditor",
"ProjectName": "Project",
"ComputerName": "Workstation",
"AppEnvironment": "datacollector-binary",
"LoadingSeconds": 3.051511
}
{
"AppID": "SomethingUniqueToCompany",
"UserID": "regnerblokandersen",
"P4Branch": "++project+main",
"EventName": "Performance.Loading",
"FirstTime": true,
"SessionID": "{40FD32AB-44A1-BD58-05E0-37A2A12E5300}",
"AppVersion": "++project+main-CL-1111111",
"DateOffset": "+00:00:00.685",
"UploadType": "eteventstream",
"LoadingName": "TotalEditorStartup",
"ProjectName": "Project",
"ComputerName": "Workstation",
"AppEnvironment": "datacollector-binary",
"LoadingSeconds": 164.196052
}
using UnrealBuildTool;
public class CustomStudioAnalytics : ModuleRules
{
public CustomStudioAnalytics(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
bEnforceIWYU = true;
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Engine",
"Analytics",
"AnalyticsET",
}
);
}
}
#include "CustomStudioAnalytics.h"
#include <Analytics.h>
#include <AnalyticsET.h>
#include <IAnalyticsProviderET.h>
#include <StudioAnalytics.h>
DEFINE_LOG_CATEGORY_STATIC(LogCustomStudioAnalytics, Display, All);
void FCustomStudioAnalyticsModule::StartupModule()
{
const FAnalytics::ConfigFromIni AnalyticsConfig;
const TSharedPtr<IAnalyticsProviderET> Provider = StaticCastSharedPtr<IAnalyticsProviderET>(
FAnalyticsET::Get().CreateAnalyticsProvider(
FAnalyticsProviderConfigurationDelegate::CreateRaw(
&AnalyticsConfig,
&FAnalytics::ConfigFromIni::GetValue
)
)
);
if (!Provider)
{
UE_LOG(LogCustomStudioAnalytics, Error, TEXT("Failed create AnalyticsProviderET for CustomStudioAnalytics. Ensure required config values are set."));
return;
}
Provider->StartSession();
Provider->SetUserID(FString(FPlatformProcess::UserName(false)));
Provider->SetDefaultEventAttributes(
MakeAnalyticsEventAttributeArray(
TEXT("ComputerName"), FString(FPlatformProcess::ComputerName()),
TEXT("ProjectName"), FString(FApp::GetProjectName()),
TEXT("P4Branch"), FApp::GetBranchName()
)
);
FStudioAnalytics::SetProvider(Provider.ToSharedRef());
}
IMPLEMENT_MODULE(FCustomStudioAnalyticsModule, CustomStudioAnalytics)
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FCustomStudioAnalyticsModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
};
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "CustomStudioAnalytics",
"Description": "A simple plugin to initialize and StudioAnalytics to send to our backend.",
"Category": "Example",
"CreatedBy": "Example",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "CustomStudioAnalytics",
"Type": "Runtime",
"LoadingPhase": "EarliestPossible"
}
]
}
[AnalyticsDevelopment]
APIServerET=https://example.local
APIKeyET=SomethingUniqueToCompany
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment