Skip to content

Instantly share code, notes, and snippets.

@regner
Last active December 4, 2024 06:53
Show Gist options
  • Select an option

  • Save regner/592bb2cfca82f064ccd5322ea4c5deb8 to your computer and use it in GitHub Desktop.

Select an option

Save regner/592bb2cfca82f064ccd5322ea4c5deb8 to your computer and use it in GitHub Desktop.

Revisions

  1. regner revised this gist Oct 27, 2022. 8 changed files with 18 additions and 24 deletions.
    2 changes: 1 addition & 1 deletion example_InitializeEditor.json
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "UserID": "regnerblokandersen",
    "P4Branch": "++project+main",
    "FirstTime": true,
    "SessionID": "{40FD32AB-44A1-BD58-05E0-37A2A12E5300}",
    4 changes: 2 additions & 2 deletions example_LoadMap.json
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "MapName": "M_POC",
    "UserID": "regnerblokandersen",
    "MapName": "Map",
    "P4Branch": "++project+main",
    "EventName": "Performance.Loading",
    "FirstTime": true,
    4 changes: 2 additions & 2 deletions example_OpenAssetEditor.json
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,8 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "UserID": "regnerblokandersen",
    "P4Branch": "++project+main",
    "AssetPath": "World /Game/Maps/M_Map.M_Map",
    "AssetPath": "World /Game/Maps/Map",
    "AssetType": "World",
    "EventName": "Performance.Loading",
    "SessionID": "{4D8224C9-4978-A127-A6EC-2A87FC619BC3}",
    2 changes: 1 addition & 1 deletion example_TotalEditorStartup.json
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "UserID": "regnerblokandersen",
    "P4Branch": "++project+main",
    "EventName": "Performance.Loading",
    "FirstTime": true,
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,8 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    using UnrealBuildTool;

    public class SDStudioAnalytics : ModuleRules
    public class CustomStudioAnalytics : ModuleRules
    {
    public SDStudioAnalytics(ReadOnlyTargetRules Target) : base(Target)
    public CustomStudioAnalytics(ReadOnlyTargetRules Target) : base(Target)
    {
    PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
    bEnforceIWYU = true;
    Original file line number Diff line number Diff line change
    @@ -1,14 +1,12 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    #include "SDStudioAnalytics.h"
    #include "CustomStudioAnalytics.h"
    #include <Analytics.h>
    #include <AnalyticsET.h>
    #include <IAnalyticsProviderET.h>
    #include <StudioAnalytics.h>

    DEFINE_LOG_CATEGORY_STATIC(LogSDStudioAnalytics, Display, All);
    DEFINE_LOG_CATEGORY_STATIC(LogCustomStudioAnalytics, Display, All);

    void FSDStudioAnalyticsModule::StartupModule()
    void FCustomStudioAnalyticsModule::StartupModule()
    {
    const FAnalytics::ConfigFromIni AnalyticsConfig;
    const TSharedPtr<IAnalyticsProviderET> Provider = StaticCastSharedPtr<IAnalyticsProviderET>(
    @@ -22,7 +20,7 @@ void FSDStudioAnalyticsModule::StartupModule()

    if (!Provider)
    {
    UE_LOG(LogSDStudioAnalytics, Error, TEXT("Failed create AnalyticsProviderET for SDStudioAnalytics. Ensure required config values are set."));
    UE_LOG(LogCustomStudioAnalytics, Error, TEXT("Failed create AnalyticsProviderET for CustomStudioAnalytics. Ensure required config values are set."));
    return;
    }

    @@ -39,4 +37,4 @@ void FSDStudioAnalyticsModule::StartupModule()
    FStudioAnalytics::SetProvider(Provider.ToSharedRef());
    }

    IMPLEMENT_MODULE(FSDStudioAnalyticsModule, SDStudioAnalytics)
    IMPLEMENT_MODULE(FCustomStudioAnalyticsModule, CustomStudioAnalytics)
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,9 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    #pragma once

    #include "CoreMinimal.h"
    #include "Modules/ModuleManager.h"

    class FSDStudioAnalyticsModule : public IModuleInterface
    class FCustomStudioAnalyticsModule : public IModuleInterface
    {
    public:
    /** IModuleInterface implementation */
    Original file line number Diff line number Diff line change
    @@ -2,10 +2,10 @@
    "FileVersion": 3,
    "Version": 1,
    "VersionName": "1.0",
    "FriendlyName": "SDStudioAnalytics",
    "FriendlyName": "CustomStudioAnalytics",
    "Description": "A simple plugin to initialize and StudioAnalytics to send to our backend.",
    "Category": "Splash Damage",
    "CreatedBy": "Splash Damage",
    "Category": "Example",
    "CreatedBy": "Example",
    "CreatedByURL": "",
    "DocsURL": "",
    "MarketplaceURL": "",
    @@ -16,7 +16,7 @@
    "Installed": false,
    "Modules": [
    {
    "Name": "SDStudioAnalytics",
    "Name": "CustomStudioAnalytics",
    "Type": "Runtime",
    "LoadingPhase": "EarliestPossible"
    }
  2. regner renamed this gist Oct 27, 2022. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. regner revised this gist Oct 27, 2022. 13 changed files with 0 additions and 0 deletions.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
    File renamed without changes.
  4. regner created this gist Oct 27, 2022.
    3 changes: 3 additions & 0 deletions DefaultEngine.ini
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    [AnalyticsDevelopment]
    APIServerET=https://example.local
    APIKeyET=SomethingUniqueToCompany
    9 changes: 9 additions & 0 deletions Dockerfile
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    FROM python:3.10-slim-buster

    WORKDIR /app

    COPY reiquirements.txt main.py

    RUN pip install -r requirements.txt

    CMD [ "python", "waitress_server" ]
    15 changes: 15 additions & 0 deletions InitializeEditor.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "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
    }
    17 changes: 17 additions & 0 deletions LoadMap.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "MapName": "M_POC",
    "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
    }
    17 changes: 17 additions & 0 deletions OpenAssetEditor.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "P4Branch": "++project+main",
    "AssetPath": "World /Game/Maps/M_Map.M_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
    }
    22 changes: 22 additions & 0 deletions SDStudioAnalytics.Build.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    using UnrealBuildTool;

    public class SDStudioAnalytics : ModuleRules
    {
    public SDStudioAnalytics(ReadOnlyTargetRules Target) : base(Target)
    {
    PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
    bEnforceIWYU = true;

    PrivateDependencyModuleNames.AddRange(
    new string[]
    {
    "Core",
    "Engine",
    "Analytics",
    "AnalyticsET",
    }
    );
    }
    }
    42 changes: 42 additions & 0 deletions SDStudioAnalytics.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    #include "SDStudioAnalytics.h"
    #include <Analytics.h>
    #include <AnalyticsET.h>
    #include <IAnalyticsProviderET.h>
    #include <StudioAnalytics.h>

    DEFINE_LOG_CATEGORY_STATIC(LogSDStudioAnalytics, Display, All);

    void FSDStudioAnalyticsModule::StartupModule()
    {
    const FAnalytics::ConfigFromIni AnalyticsConfig;
    const TSharedPtr<IAnalyticsProviderET> Provider = StaticCastSharedPtr<IAnalyticsProviderET>(
    FAnalyticsET::Get().CreateAnalyticsProvider(
    FAnalyticsProviderConfigurationDelegate::CreateRaw(
    &AnalyticsConfig,
    &FAnalytics::ConfigFromIni::GetValue
    )
    )
    );

    if (!Provider)
    {
    UE_LOG(LogSDStudioAnalytics, Error, TEXT("Failed create AnalyticsProviderET for SDStudioAnalytics. 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(FSDStudioAnalyticsModule, SDStudioAnalytics)
    13 changes: 13 additions & 0 deletions SDStudioAnalytics.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    // Copyright Splash Damage, Ltd. - All Rights Reserved.

    #pragma once

    #include "CoreMinimal.h"
    #include "Modules/ModuleManager.h"

    class FSDStudioAnalyticsModule : public IModuleInterface
    {
    public:
    /** IModuleInterface implementation */
    virtual void StartupModule() override;
    };
    24 changes: 24 additions & 0 deletions SDStudioAnalytics.uplugin
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    {
    "FileVersion": 3,
    "Version": 1,
    "VersionName": "1.0",
    "FriendlyName": "SDStudioAnalytics",
    "Description": "A simple plugin to initialize and StudioAnalytics to send to our backend.",
    "Category": "Splash Damage",
    "CreatedBy": "Splash Damage",
    "CreatedByURL": "",
    "DocsURL": "",
    "MarketplaceURL": "",
    "SupportURL": "",
    "CanContainContent": false,
    "IsBetaVersion": false,
    "IsExperimentalVersion": false,
    "Installed": false,
    "Modules": [
    {
    "Name": "SDStudioAnalytics",
    "Type": "Runtime",
    "LoadingPhase": "EarliestPossible"
    }
    ]
    }
    16 changes: 16 additions & 0 deletions TotalEditorStartup.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    {
    "AppID": "SomethingUniqueToCompany",
    "UserID": "regner.blokandersen",
    "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
    }
    79 changes: 79 additions & 0 deletions analytics_backend.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,79 @@
    """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)
    5 changes: 5 additions & 0 deletions requirements.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    flask==2.2.2
    psycopg2==2.9.5
    sentry-sdk[flask]==1.10.1
    sqlalchemy==1.4.42
    waitress==2.1.2
    17 changes: 17 additions & 0 deletions waitress_server.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    """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,
    )