Implement telemetry system with data collection and reporting
Signed-off-by: Dmitrii <computer@yawaflua.tech> Took 20 minutes
This commit is contained in:
+2
-1
@@ -20,7 +20,8 @@ hs_err_pid*
|
||||
# Common working directory
|
||||
run
|
||||
|
||||
backend/SpMega.Backend/src/*
|
||||
backend/SpMega.Backend/bin/*
|
||||
backend/SpMega.Backend/obj/*
|
||||
backend/SpMega.Backend/appsettings.json
|
||||
backend/SpMega.Backend/appsettings.Development.json
|
||||
backend/SpMega.Backend/SpMega.Backend.http
|
||||
@@ -20,6 +20,7 @@ namespace SpMega.Backend.Controllers.v1;
|
||||
|
||||
public record GetSessionIdBody(string userName, Guid userUUID);
|
||||
public record ValidateSessionBody(string sessionId, Guid userUUID);
|
||||
|
||||
[Route("api/v1/auth")]
|
||||
[ApiController]
|
||||
public class AuthController(AppDbContext dbContext, TokenService tokenService, ILogger<AuthController> logger) : ControllerBase
|
||||
@@ -67,7 +68,7 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
|
||||
|
||||
var resp = await httpClient.SendAsync(request);
|
||||
if (resp.StatusCode != HttpStatusCode.OK) throw new Exception("Mojang response is not OK");
|
||||
Console.WriteLine(await resp.Content.ReadAsStringAsync());
|
||||
|
||||
var dto = await resp.Content.ReadFromJsonAsync<MojangDto>();
|
||||
if (dto == null || dto.Name != session.UserName || Guid.Parse(dto.Id) != session.UserId) throw new Exception("Session expired, or dto is not acceptable.");
|
||||
var token = tokenService.GenerateAccessToken(session.UserName, body.userUUID);
|
||||
@@ -75,6 +76,9 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
|
||||
if (user != null)
|
||||
{
|
||||
user.Token = token;
|
||||
user.Username = session.UserName;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
dbContext.Update(user);
|
||||
}
|
||||
else
|
||||
@@ -84,6 +88,7 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
|
||||
Id = body.userUUID,
|
||||
Username = session.UserName,
|
||||
Token = token,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
await dbContext.AddAsync(user);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SpMega.Backend.Persistent.Models.DTO;
|
||||
using SpMega.Backend.Persistent.Models.Users;
|
||||
|
||||
namespace SpMega.Backend.Controllers.v1;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/telemetry")]
|
||||
public class TelemetryController : ControllerBase
|
||||
{
|
||||
private static readonly ActivitySource Source = new("SpMega.ModTelemetry");
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public IActionResult Post([FromBody] ModTelemetryBatchDto batch)
|
||||
{
|
||||
if (batch?.Events == null || batch.Events.Count == 0)
|
||||
{
|
||||
return Ok(new { received = 0 });
|
||||
}
|
||||
|
||||
var user = HttpContext.Items["@me"] as User;
|
||||
var userId = user?.Id.ToString() ?? User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "anonymous";
|
||||
var sessionId = !string.IsNullOrEmpty(batch.SessionId) ? batch.SessionId : Guid.NewGuid().ToString("N");
|
||||
|
||||
foreach (var e in batch.Events)
|
||||
{
|
||||
using var activity = Source.StartActivity("mod.telemetry.event", ActivityKind.Internal);
|
||||
if (activity is null) continue;
|
||||
|
||||
activity.SetTag("user.id", userId);
|
||||
activity.SetTag("session.id", sessionId);
|
||||
activity.SetTag("client.version", batch.ClientVersion ?? string.Empty);
|
||||
activity.SetTag("event.type", e.EventType);
|
||||
activity.SetTag("event.timestamp", e.Timestamp.ToString("O"));
|
||||
activity.SetTag("event.payload", JsonSerializer.Serialize(e.Payload));
|
||||
activity.SetTag("batch.sent_at", batch.SentAt.ToString("O"));
|
||||
}
|
||||
|
||||
return Ok(new { received = batch.Events.Count });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SpMega.Backend.Middleware;
|
||||
|
||||
public class RequestTelemetryMiddleware(RequestDelegate next, ILogger<RequestTelemetryMiddleware> logger)
|
||||
{
|
||||
private static readonly ActivitySource Source = new("SpMega.RequestTelemetry");
|
||||
private const string RequestIdHeader = "X-Request-Id";
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value ?? string.Empty;
|
||||
if (path.Equals("/api/v1/telemetry", StringComparison.OrdinalIgnoreCase)
|
||||
&& context.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var requestId = context.Request.Headers[RequestIdHeader].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
||||
context.Response.Headers[RequestIdHeader] = requestId;
|
||||
|
||||
using var activity = Source.StartActivity("http.request", ActivityKind.Server);
|
||||
activity?.SetTag("request.id", requestId);
|
||||
activity?.SetTag("http.method", context.Request.Method);
|
||||
activity?.SetTag("http.path", path);
|
||||
activity?.SetTag("http.scheme", context.Request.Scheme);
|
||||
activity?.SetTag("http.host", context.Request.Host.Value ?? string.Empty);
|
||||
activity?.SetTag("client.ip", context.Connection.RemoteIpAddress?.ToString());
|
||||
activity?.SetTag("http.user_agent", context.Request.Headers.UserAgent.ToString());
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
activity?.SetTag("http.status_code", context.Response.StatusCode);
|
||||
activity?.SetTag("http.duration_ms", stopwatch.ElapsedMilliseconds);
|
||||
|
||||
var userId = context.User?.Identity?.IsAuthenticated == true
|
||||
? context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||
: null;
|
||||
if (userId is not null)
|
||||
activity?.SetTag("user.id", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.EntityFrameworkCore.Extensions;
|
||||
using SpMega.Backend.Persistent.Models.Telemetry;
|
||||
using SpMega.Backend.Persistent.Models.Transactions;
|
||||
using SpMega.Backend.Persistent.Models.Users;
|
||||
using SpMega.Backend.Services;
|
||||
@@ -11,6 +12,8 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
internal DbSet<Transaction> Transactions { get; set; }
|
||||
internal DbSet<User> Users { get; set; }
|
||||
internal DbSet<UserSession> UserSessions { get; set; }
|
||||
internal DbSet<ModTelemetryDocument> ModTelemetry { get; set; }
|
||||
internal DbSet<BackendRequestTelemetryDocument> BackendRequestTelemetry { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SpMega.Backend.Persistent.Models.DTO;
|
||||
|
||||
public record ModTelemetryEventDto(
|
||||
string EventType,
|
||||
DateTimeOffset Timestamp,
|
||||
JsonElement Payload
|
||||
);
|
||||
|
||||
public record ModTelemetryBatchDto(
|
||||
string ClientVersion,
|
||||
string SessionId,
|
||||
DateTimeOffset SentAt,
|
||||
List<ModTelemetryEventDto> Events
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace SpMega.Backend.Persistent.Models.Telemetry;
|
||||
|
||||
public class BackendRequestTelemetryDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string RequestId { get; set; } = string.Empty;
|
||||
public string Method { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string Scheme { get; set; } = string.Empty;
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public string? ClientIp { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public long DurationMs { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace SpMega.Backend.Persistent.Models.Telemetry;
|
||||
|
||||
public class ModTelemetryDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
public string ClientVersion { get; set; } = string.Empty;
|
||||
public DateTimeOffset SentAt { get; set; }
|
||||
public DateTimeOffset ReceivedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<ModTelemetryEventDocument> Events { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ModTelemetryEventDocument
|
||||
{
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public string PayloadJson { get; set; } = "{}";
|
||||
}
|
||||
@@ -3,7 +3,10 @@ using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using SpMega.Backend.Authetication;
|
||||
using SpMega.Backend.Middleware;
|
||||
using SpMega.Backend.Persistent.Database;
|
||||
using SpMega.Backend.Services;
|
||||
|
||||
@@ -56,18 +59,33 @@ public class Program
|
||||
.RequireAuthenticatedUser()
|
||||
.Build());
|
||||
|
||||
var serviceCollection = new ServiceCollection();
|
||||
var mongoClient =
|
||||
new MongoClient(builder.Configuration.GetValue<string>("Mongo") ?? "mongodb://curiosity:27018");
|
||||
serviceCollection.AddSingleton<IMongoClient>(mongoClient);
|
||||
builder.Services.AddSingleton<IMongoClient>(mongoClient);
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>((_, options) =>
|
||||
{
|
||||
options.UseMongoDB(mongoClient, "spmega");
|
||||
});
|
||||
|
||||
var otlpEndpoint = conf["Otlp:Endpoint"] ?? "http://localhost:4317";
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.ConfigureResource(res => res.AddService("SpMega.Backend"))
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing
|
||||
.AddSource("SpMega.ModTelemetry")
|
||||
.AddSource("SpMega.RequestTelemetry")
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddOtlpExporter(opts =>
|
||||
{
|
||||
opts.Endpoint = new Uri(otlpEndpoint);
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseMiddleware<RequestTelemetryMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), appBuilder =>
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
|
||||
<PackageReference Include="MongoDB.EntityFrameworkCore" Version="10.0.2" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="spworlds-api" Version="1.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v10.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v10.0": {
|
||||
"SpMega.Backend/1.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.9",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "10.0.9",
|
||||
"Microsoft.IdentityModel.Tokens": "8.19.1",
|
||||
"MongoDB.EntityFrameworkCore": "10.0.2",
|
||||
"System.IdentityModel.Tokens.Jwt": "8.19.1",
|
||||
"spworlds-api": "1.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"SpMega.Backend.dll": {}
|
||||
}
|
||||
},
|
||||
"DnsClient/1.6.1": {
|
||||
"runtime": {
|
||||
"lib/net5.0/DnsClient.dll": {
|
||||
"assemblyVersion": "1.6.1.0",
|
||||
"fileVersion": "1.6.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Bcl.Cryptography/10.0.2": {
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.Bcl.Cryptography.dll": {
|
||||
"assemblyVersion": "10.0.0.2",
|
||||
"fileVersion": "10.0.225.61305"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/10.0.9": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.9"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.EntityFrameworkCore.dll": {
|
||||
"assemblyVersion": "10.0.9.0",
|
||||
"fileVersion": "10.0.926.27113"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.9": {
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
|
||||
"assemblyVersion": "10.0.9.0",
|
||||
"fileVersion": "10.0.926.27113"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational/10.0.9": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "10.0.9"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.EntityFrameworkCore.Relational.dll": {
|
||||
"assemblyVersion": "10.0.9.0",
|
||||
"fileVersion": "10.0.926.27113"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.IdentityModel.Abstractions.dll": {
|
||||
"assemblyVersion": "8.19.1.0",
|
||||
"fileVersion": "8.19.1.26153"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.IdentityModel.JsonWebTokens.dll": {
|
||||
"assemblyVersion": "8.19.1.0",
|
||||
"fileVersion": "8.19.1.26153"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.Logging/8.19.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Abstractions": "8.19.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.IdentityModel.Logging.dll": {
|
||||
"assemblyVersion": "8.19.1.0",
|
||||
"fileVersion": "8.19.1.26153"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.Cryptography": "10.0.2",
|
||||
"Microsoft.IdentityModel.Logging": "8.19.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/Microsoft.IdentityModel.Tokens.dll": {
|
||||
"assemblyVersion": "8.19.1.0",
|
||||
"fileVersion": "8.19.1.26153"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MongoDB.Bson/3.9.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/MongoDB.Bson.dll": {
|
||||
"assemblyVersion": "3.9.0.0",
|
||||
"fileVersion": "3.9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MongoDB.Driver/3.9.0": {
|
||||
"dependencies": {
|
||||
"DnsClient": "1.6.1",
|
||||
"MongoDB.Bson": "3.9.0",
|
||||
"SharpCompress": "0.48.1",
|
||||
"Snappier": "1.3.1",
|
||||
"ZstdSharp.Port": "0.7.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/MongoDB.Driver.dll": {
|
||||
"assemblyVersion": "3.9.0.0",
|
||||
"fileVersion": "3.9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MongoDB.EntityFrameworkCore/10.0.2": {
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "10.0.9",
|
||||
"MongoDB.Driver": "3.9.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/MongoDB.EntityFrameworkCore.dll": {
|
||||
"assemblyVersion": "10.0.2.0",
|
||||
"fileVersion": "10.0.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SharpCompress/0.48.1": {
|
||||
"runtime": {
|
||||
"lib/net10.0/SharpCompress.dll": {
|
||||
"assemblyVersion": "0.48.1.0",
|
||||
"fileVersion": "0.48.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Snappier/1.3.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Snappier.dll": {
|
||||
"assemblyVersion": "1.3.1.0",
|
||||
"fileVersion": "1.3.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spworlds-api/1.0.3": {
|
||||
"runtime": {
|
||||
"lib/net8.0/spworlds-api.dll": {
|
||||
"assemblyVersion": "1.0.3.0",
|
||||
"fileVersion": "1.0.3.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.JsonWebTokens": "8.19.1",
|
||||
"Microsoft.IdentityModel.Tokens": "8.19.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net10.0/System.IdentityModel.Tokens.Jwt.dll": {
|
||||
"assemblyVersion": "8.19.1.0",
|
||||
"fileVersion": "8.19.1.26153"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ZstdSharp.Port/0.7.3": {
|
||||
"runtime": {
|
||||
"lib/net7.0/ZstdSharp.dll": {
|
||||
"assemblyVersion": "0.7.3.0",
|
||||
"fileVersion": "0.7.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"SpMega.Backend/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DnsClient/1.6.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==",
|
||||
"path": "dnsclient/1.6.1",
|
||||
"hashPath": "dnsclient.1.6.1.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Bcl.Cryptography/10.0.2": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-LG9Yll3B5aNpxv0+D47g6LiOiKBIlodhcHdQwcYzo8VeexFLGqx5ymetmA2aBRyo9cCcWsQWrFsdbsr8LvmWDw==",
|
||||
"path": "microsoft.bcl.cryptography/10.0.2",
|
||||
"hashPath": "microsoft.bcl.cryptography.10.0.2.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore/10.0.9": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-tu85SRzOT021V7EQlViCiAE7TqldVn469Y6lt5TEn/+XC4/MeNCHgMRSxqYuWqvF4zAQZUhCmtNEZuM3ss4LeA==",
|
||||
"path": "microsoft.entityframeworkcore/10.0.9",
|
||||
"hashPath": "microsoft.entityframeworkcore.10.0.9.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions/10.0.9": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-GRMaiPkqYna/gCsyDffYDWmefGPC3hDrdMw+2rrGcQwhs6uZOsaMQXMJnoXQ35tx9SkBV2ieRRU9N/jLOO6BZw==",
|
||||
"path": "microsoft.entityframeworkcore.abstractions/10.0.9",
|
||||
"hashPath": "microsoft.entityframeworkcore.abstractions.10.0.9.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational/10.0.9": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-dEoyYKgiaZHHgOFm1WMWm1sFEsEuhPWufX4L9PekKtqd/RaIcPjkCjvbrVvJtApErb5wPSJhYvnTlxhH+p9h2g==",
|
||||
"path": "microsoft.entityframeworkcore.relational/10.0.9",
|
||||
"hashPath": "microsoft.entityframeworkcore.relational.10.0.9.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.IdentityModel.Abstractions/8.19.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-gFA8THIk23uNF/vMdOHnjIdXD1LyA2g12cHzMJ+Xag6WpgWLw6E/6uCXxvA0gp9d2yAvkRt3xzFzMUiO/hofnQ==",
|
||||
"path": "microsoft.identitymodel.abstractions/8.19.1",
|
||||
"hashPath": "microsoft.identitymodel.abstractions.8.19.1.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.IdentityModel.JsonWebTokens/8.19.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-6eeY+y2QFyjj3XnCz/8gJdoP5smYHTS9ow1bw2nsZzDIPjPhBZlackYTIduSMipVpxnoT/B62LkrXX2jPggOXg==",
|
||||
"path": "microsoft.identitymodel.jsonwebtokens/8.19.1",
|
||||
"hashPath": "microsoft.identitymodel.jsonwebtokens.8.19.1.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.IdentityModel.Logging/8.19.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-H+sMrMpdbWnwkQnpb/ESkQovtOgdefmj0ecGCcP40mDKzE5i4dUYkH6599M9mWYFNGNJnTp92l/9wLubYXWimw==",
|
||||
"path": "microsoft.identitymodel.logging/8.19.1",
|
||||
"hashPath": "microsoft.identitymodel.logging.8.19.1.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.IdentityModel.Tokens/8.19.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KDiuSLXud2AFVNAOottd8ztVysfPeHyr4r8gofU3/VKUXlI7oytzGTnPsNJ/B3nui17rgz8wAdWNJOtzPjkUxw==",
|
||||
"path": "microsoft.identitymodel.tokens/8.19.1",
|
||||
"hashPath": "microsoft.identitymodel.tokens.8.19.1.nupkg.sha512"
|
||||
},
|
||||
"MongoDB.Bson/3.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-J6vB61zKwMSfxbkN+/amGvj1qVMDKrKjV3kmoOWttMcv8JJScLDCFh89FyL3f2lDUyJrnfgZCR6/KX+07e99eg==",
|
||||
"path": "mongodb.bson/3.9.0",
|
||||
"hashPath": "mongodb.bson.3.9.0.nupkg.sha512"
|
||||
},
|
||||
"MongoDB.Driver/3.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-XKUa+y5RtNH1iInfxj3Y7c1FN1BQ16/7hFxoqU6fzc3+BKM1D3mGa+pB/yBbAk8jNxf7+JWEnCfuQOyCo7dQLg==",
|
||||
"path": "mongodb.driver/3.9.0",
|
||||
"hashPath": "mongodb.driver.3.9.0.nupkg.sha512"
|
||||
},
|
||||
"MongoDB.EntityFrameworkCore/10.0.2": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-GBN9+OAoIyBEgVPlYTXW3tyIsnpGPCA5AWZPy+X7ca3zJu+2Lli9FODZWl//w0yZHqwGwsLUL6XhcPPEaBYvBQ==",
|
||||
"path": "mongodb.entityframeworkcore/10.0.2",
|
||||
"hashPath": "mongodb.entityframeworkcore.10.0.2.nupkg.sha512"
|
||||
},
|
||||
"SharpCompress/0.48.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-SqGaVniGG943Gph/gHhUQUiZPyC7y0tXZyMf0/B2oGsMav9dqs7JJOuUA+xOkwKYaWM2TM7aZQjNK91f4bX71A==",
|
||||
"path": "sharpcompress/0.48.1",
|
||||
"hashPath": "sharpcompress.0.48.1.nupkg.sha512"
|
||||
},
|
||||
"Snappier/1.3.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-DOdDQiO8YZ5rBtVLY+6CmR1yp9WYoJRgEEktPBrR0tEj9QO2djA/zv0O3DX0OZpEAfosbY8pytQ9tQUogwQsEA==",
|
||||
"path": "snappier/1.3.1",
|
||||
"hashPath": "snappier.1.3.1.nupkg.sha512"
|
||||
},
|
||||
"spworlds-api/1.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rLQsC3MiXBfPKf3v45Qcrj0yypfZktTPuT/eGUJpcAlSZix1FBSRUbQVtfXbA8BHGIqn7nhZMVetaP3dmk+n1g==",
|
||||
"path": "spworlds-api/1.0.3",
|
||||
"hashPath": "spworlds-api.1.0.3.nupkg.sha512"
|
||||
},
|
||||
"System.IdentityModel.Tokens.Jwt/8.19.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2VHcRtT95GAcW1E3aVBLvL2rAAMxKHXKMXKXFyWzwgkdFXZPMMvP8tVOfnRydL4vTr1RirNuGC6T8VSEF2YsPQ==",
|
||||
"path": "system.identitymodel.tokens.jwt/8.19.1",
|
||||
"hashPath": "system.identitymodel.tokens.jwt.8.19.1.nupkg.sha512"
|
||||
},
|
||||
"ZstdSharp.Port/0.7.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U9Ix4l4cl58Kzz1rJzj5hoVTjmbx1qGMwzAcbv1j/d3NzrFaESIurQyg+ow4mivCgkE3S413y+U9k4WdnEIkRA==",
|
||||
"path": "zstdsharp.port/0.7.3",
|
||||
"hashPath": "zstdsharp.port.0.7.3.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net10.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "10.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "10.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.GC.Server": true,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Version": 1,
|
||||
"ManifestType": "Build",
|
||||
"Endpoints": []
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APaymentWrapper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F28d7b99649ed43d899c3d8518f41c1976400_003F94_003Fbc0e580e_003FPaymentWrapper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASPWorlds_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F28d7b99649ed43d899c3d8518f41c1976400_003F64_003Fd3041423_003FSPWorlds_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||
@@ -35,6 +35,10 @@ public class ModMenuIntegration implements ModMenuApi {
|
||||
final boolean[] signQuickPayEnabled = {current.signQuickPayEnabled()};
|
||||
final GpsHudPosition[] gpsPosition = {current.gpsPosition()};
|
||||
|
||||
final boolean[] telemetryEnabled = {current.telemetryEnabled()};
|
||||
final int[] telemetryIntervalSeconds = {current.telemetryIntervalSeconds()};
|
||||
final boolean[] telemetryCollectSystemInfo = {current.telemetryCollectSystemInfo()};
|
||||
|
||||
general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_domain"), current.apiDomain())
|
||||
.setDefaultValue(ModConfig.DEFAULT_API_DOMAIN)
|
||||
.setSaveConsumer(newValue -> apiDomain[0] = newValue)
|
||||
@@ -80,6 +84,29 @@ public class ModMenuIntegration implements ModMenuApi {
|
||||
.setSaveConsumer(newValue -> gpsPosition[0] = newValue)
|
||||
.build());
|
||||
|
||||
// Telemetry settings
|
||||
ConfigCategory telemetry = builder.getOrCreateCategory(Text.translatable("category.spmega.telemetry"));
|
||||
|
||||
telemetry.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.telemetry_enabled"), current.telemetryEnabled())
|
||||
.setDefaultValue(ModConfig.DEFAULT_TELEMETRY_ENABLED)
|
||||
.setTooltip(Text.translatable("tooltip.spmega.telemetry_enabled"))
|
||||
.setSaveConsumer(newValue -> telemetryEnabled[0] = newValue)
|
||||
.build());
|
||||
|
||||
telemetry.addEntry(entryBuilder.startIntField(Text.translatable("option.spmega.telemetry_interval"), current.telemetryIntervalSeconds())
|
||||
.setDefaultValue(ModConfig.DEFAULT_TELEMETRY_INTERVAL_SECONDS)
|
||||
.setMin(10)
|
||||
.setMax(600)
|
||||
.setTooltip(Text.translatable("tooltip.spmega.telemetry_interval"))
|
||||
.setSaveConsumer(newValue -> telemetryIntervalSeconds[0] = newValue)
|
||||
.build());
|
||||
|
||||
telemetry.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.telemetry_system_info"), current.telemetryCollectSystemInfo())
|
||||
.setDefaultValue(ModConfig.DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO)
|
||||
.setTooltip(Text.translatable("tooltip.spmega.telemetry_system_info"))
|
||||
.setSaveConsumer(newValue -> telemetryCollectSystemInfo[0] = newValue)
|
||||
.build());
|
||||
|
||||
builder.setSavingRunnable(() -> {
|
||||
boolean gpsEnabledVal = true;
|
||||
if (SPMega.getConfig() != null) {
|
||||
@@ -91,7 +118,10 @@ public class ModMenuIntegration implements ModMenuApi {
|
||||
allowBackend[0],
|
||||
signQuickPayEnabled[0],
|
||||
gpsEnabledVal,
|
||||
gpsPosition[0]
|
||||
gpsPosition[0],
|
||||
telemetryEnabled[0],
|
||||
telemetryIntervalSeconds[0],
|
||||
telemetryCollectSystemInfo[0]
|
||||
);
|
||||
SPMega.setConfig(updated);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ package git.yawaflua.tech.spmega.client;
|
||||
import git.yawaflua.tech.spmega.ModConfig;
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.client.qr.QRCodeScanner;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.*;
|
||||
import git.yawaflua.tech.spmega.client.ui.GpsHudRenderer;
|
||||
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||
@@ -100,6 +101,7 @@ public class SPMegaClient implements ClientModInitializer {
|
||||
}
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
PerformanceSampler.instance().tick();
|
||||
UiNotifications.instance().tick();
|
||||
while (openBankMenuKeyBinding.wasPressed()) {
|
||||
UiOpeners.openMainMenu(client);
|
||||
@@ -117,7 +119,10 @@ public class SPMegaClient implements ClientModInitializer {
|
||||
current.allowBackend(),
|
||||
current.signQuickPayEnabled(),
|
||||
GpsHudRenderer.instance().isEnabled(),
|
||||
current.gpsPosition()
|
||||
current.gpsPosition(),
|
||||
current.telemetryEnabled(),
|
||||
current.telemetryIntervalSeconds(),
|
||||
current.telemetryCollectSystemInfo()
|
||||
);
|
||||
SPMega.setConfig(updated);
|
||||
}
|
||||
@@ -232,6 +237,27 @@ public class SPMegaClient implements ClientModInitializer {
|
||||
System.err.println("[SPMEGA] Error during async authenticate: " + throwable.getMessage());
|
||||
return null;
|
||||
});
|
||||
|
||||
// Telemetry init
|
||||
long clientInitStart = System.nanoTime();
|
||||
ModConfig cfg = SPMega.getConfig();
|
||||
int interval = (cfg != null) ? cfg.telemetryIntervalSeconds() : ModConfig.DEFAULT_TELEMETRY_INTERVAL_SECONDS;
|
||||
SystemInfoCollector.instance().init();
|
||||
TelemetrySender.instance().start(interval);
|
||||
long clientInitMs = (System.nanoTime() - clientInitStart) / 1_000_000L;
|
||||
com.google.gson.JsonObject initPayload = new com.google.gson.JsonObject();
|
||||
initPayload.addProperty("phase", "client_init");
|
||||
initPayload.addProperty("durationMs", clientInitMs);
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("lifecycle", initPayload));
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
com.google.gson.JsonObject deinitPayload = new com.google.gson.JsonObject();
|
||||
deinitPayload.addProperty("phase", "client_deinit");
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("lifecycle", deinitPayload));
|
||||
TelemetrySender.instance().flush();
|
||||
TelemetrySender.instance().stop();
|
||||
}, "SPMega-Telemetry-Shutdown"));
|
||||
|
||||
System.out.println("Author of SPMega make it with 4 cans of monster");
|
||||
System.out.println("If u want to see more updates - give me like 10 shekels for monster plzzz");
|
||||
System.out.println("Initialized beshalom! Tieie tovim!");
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package git.yawaflua.tech.spmega.api;
|
||||
package git.yawaflua.tech.spmega.client.api;
|
||||
|
||||
import com.google.gson.*;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.InstrumentedHttpClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -13,12 +13,12 @@ import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public final class SPWorldsApiClient {
|
||||
private final HttpClient httpClient;
|
||||
private final InstrumentedHttpClient httpClient;
|
||||
private final Gson gson;
|
||||
private final String baseUrl;
|
||||
|
||||
public SPWorldsApiClient(String baseUrl) {
|
||||
this.httpClient = HttpClient.newHttpClient();
|
||||
this.httpClient = new InstrumentedHttpClient();
|
||||
this.gson = new Gson();
|
||||
this.baseUrl = normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
@@ -161,7 +161,7 @@ public final class SPWorldsApiClient {
|
||||
}
|
||||
|
||||
private String send(HttpRequest request) throws IOException, InterruptedException {
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
int statusCode = response.statusCode();
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
System.out.println(response.body());
|
||||
@@ -4,6 +4,8 @@ import com.google.zxing.*;
|
||||
import com.google.zxing.common.GlobalHistogramBinarizer;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.multi.GenericMultipleBarcodeReader;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.TelemetryCollector;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.TelemetryEvent;
|
||||
import git.yawaflua.tech.spmega.client.ui.QRcodeAcceptScreen;
|
||||
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
@@ -30,6 +32,7 @@ public class QRCodeScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
long startNs = System.nanoTime();
|
||||
try {
|
||||
ScreenshotRecorder.takeScreenshot(client.getFramebuffer(), nativeImage -> {
|
||||
try {
|
||||
@@ -39,10 +42,13 @@ public class QRCodeScanner {
|
||||
notifications.show(Text.translatable("message.spmega.qr.capture_failed"));
|
||||
}
|
||||
});
|
||||
recordQrEvent(false, false, null, "capture_failed", startNs);
|
||||
return;
|
||||
}
|
||||
|
||||
String result = decodeQRCode(nativeImageToBufferedImage(nativeImage));
|
||||
long scanNs = System.nanoTime();
|
||||
boolean didLag = (scanNs - startNs) > 50_000_000L;
|
||||
client.execute(() -> {
|
||||
if (client.player == null) {
|
||||
return;
|
||||
@@ -57,6 +63,7 @@ public class QRCodeScanner {
|
||||
client.player.sendMessage(Text.translatable("message.spmega.qr.found_link", clickableLink), false);
|
||||
client.setScreen(new QRcodeAcceptScreen(result, client.currentScreen));
|
||||
});
|
||||
recordQrEvent(result != null, didLag, result, null, startNs);
|
||||
} finally {
|
||||
if (nativeImage != null) {
|
||||
nativeImage.close();
|
||||
@@ -65,9 +72,24 @@ public class QRCodeScanner {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
notifications.show(Text.translatable("message.spmega.qr.error"));
|
||||
recordQrEvent(false, false, null, ex.getClass().getSimpleName() + ": " + ex.getMessage(), startNs);
|
||||
}
|
||||
}
|
||||
|
||||
private static void recordQrEvent(boolean success, boolean didLag, String decodedUrl, String error, long startNs) {
|
||||
com.google.gson.JsonObject payload = new com.google.gson.JsonObject();
|
||||
payload.addProperty("success", success);
|
||||
payload.addProperty("didLag", didLag);
|
||||
payload.addProperty("durationMs", (System.nanoTime() - startNs) / 1_000_000L);
|
||||
if (decodedUrl != null) {
|
||||
payload.addProperty("decodedUrl", decodedUrl);
|
||||
}
|
||||
if (error != null) {
|
||||
payload.addProperty("error", error);
|
||||
}
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("qr_scan", payload));
|
||||
}
|
||||
|
||||
private static BufferedImage nativeImageToBufferedImage(NativeImage screenshot) {
|
||||
BufferedImage bufferedImage = new BufferedImage(
|
||||
screenshot.getWidth(),
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public record InstrumentedHttpClient(HttpClient delegate) {
|
||||
public InstrumentedHttpClient() {
|
||||
this(HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(15))
|
||||
.build());
|
||||
}
|
||||
|
||||
public static void recordHttpEvent(URI uri, String method, int statusCode,
|
||||
long durationMs, boolean success, String errorType,
|
||||
int fpsBefore, int fpsAfter) {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("target", TelemetryUriSanitizer.classifyTarget(uri));
|
||||
payload.addProperty("method", method);
|
||||
payload.addProperty("path", TelemetryUriSanitizer.sanitize(uri));
|
||||
payload.addProperty("statusCode", statusCode);
|
||||
payload.addProperty("durationMs", durationMs);
|
||||
payload.addProperty("success", success);
|
||||
if (errorType != null) {
|
||||
payload.addProperty("errorType", errorType);
|
||||
}
|
||||
payload.addProperty("fpsBefore", fpsBefore);
|
||||
payload.addProperty("fpsAfter", fpsAfter);
|
||||
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("http_request", payload));
|
||||
}
|
||||
|
||||
public HttpResponse<String> send(HttpRequest request) throws IOException, InterruptedException {
|
||||
URI uri = request.uri();
|
||||
String target = TelemetryUriSanitizer.classifyTarget(uri);
|
||||
String sanitizedPath = TelemetryUriSanitizer.sanitize(uri);
|
||||
String method = request.method();
|
||||
|
||||
int fpsBefore = PerformanceSampler.instance().currentFps();
|
||||
long startNs = System.nanoTime();
|
||||
|
||||
try {
|
||||
HttpResponse<String> response = delegate.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
||||
|
||||
recordEvent(target, method, sanitizedPath, response.statusCode(), durationMs,
|
||||
true, null, fpsBefore, fpsAfter);
|
||||
|
||||
return response;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
||||
|
||||
recordEvent(target, method, sanitizedPath, -1, durationMs,
|
||||
false, e.getClass().getSimpleName(), fpsBefore, fpsAfter);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<HttpResponse<String>> sendAsync(HttpRequest request) {
|
||||
URI uri = request.uri();
|
||||
String target = TelemetryUriSanitizer.classifyTarget(uri);
|
||||
String sanitizedPath = TelemetryUriSanitizer.sanitize(uri);
|
||||
String method = request.method();
|
||||
|
||||
int fpsBefore = PerformanceSampler.instance().currentFps();
|
||||
long startNs = System.nanoTime();
|
||||
|
||||
return delegate.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((response, throwable) -> {
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
int fpsAfter = PerformanceSampler.instance().currentFps();
|
||||
|
||||
if (throwable != null) {
|
||||
recordEvent(target, method, sanitizedPath, -1, durationMs,
|
||||
false, throwable.getClass().getSimpleName(), fpsBefore, fpsAfter);
|
||||
} else {
|
||||
recordEvent(target, method, sanitizedPath, response.statusCode(), durationMs,
|
||||
true, null, fpsBefore, fpsAfter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void recordEvent(String target, String method, String path, int statusCode,
|
||||
long durationMs, boolean success, String errorType,
|
||||
int fpsBefore, int fpsAfter) {
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("target", target);
|
||||
payload.addProperty("method", method);
|
||||
payload.addProperty("path", path);
|
||||
payload.addProperty("statusCode", statusCode);
|
||||
payload.addProperty("durationMs", durationMs);
|
||||
payload.addProperty("success", success);
|
||||
if (errorType != null) {
|
||||
payload.addProperty("errorType", errorType);
|
||||
}
|
||||
payload.addProperty("fpsBefore", fpsBefore);
|
||||
payload.addProperty("fpsAfter", fpsAfter);
|
||||
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("http_request", payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class PerformanceSampler {
|
||||
private static final PerformanceSampler INSTANCE = new PerformanceSampler();
|
||||
|
||||
private final AtomicInteger fpsSum = new AtomicInteger();
|
||||
private final AtomicInteger fpsCount = new AtomicInteger();
|
||||
private final AtomicInteger fpsMin = new AtomicInteger(Integer.MAX_VALUE);
|
||||
private final AtomicInteger fpsMax = new AtomicInteger(0);
|
||||
private final AtomicLong lastSnapshotMs = new AtomicLong(System.currentTimeMillis());
|
||||
|
||||
private PerformanceSampler() {
|
||||
}
|
||||
|
||||
public static PerformanceSampler instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
int fps = MinecraftClient.getInstance().getCurrentFps();
|
||||
fpsSum.addAndGet(fps);
|
||||
fpsCount.incrementAndGet();
|
||||
fpsMin.updateAndGet(prev -> Math.min(prev, fps));
|
||||
fpsMax.updateAndGet(prev -> Math.max(prev, fps));
|
||||
}
|
||||
|
||||
public int currentFps() {
|
||||
return MinecraftClient.getInstance().getCurrentFps();
|
||||
}
|
||||
|
||||
public void emitSnapshot() {
|
||||
int count = fpsCount.getAndSet(0);
|
||||
int sum = fpsSum.getAndSet(0);
|
||||
int min = fpsMin.getAndSet(Integer.MAX_VALUE);
|
||||
int max = fpsMax.getAndSet(0);
|
||||
long now = System.currentTimeMillis();
|
||||
long periodMs = now - lastSnapshotMs.getAndSet(now);
|
||||
|
||||
if (count == 0) return;
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("fpsAvg", sum / count);
|
||||
payload.addProperty("fpsMin", min == Integer.MAX_VALUE ? 0 : min);
|
||||
payload.addProperty("fpsMax", max);
|
||||
payload.addProperty("sampleCount", count);
|
||||
payload.addProperty("periodMs", periodMs);
|
||||
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
payload.addProperty("usedMemoryMb", (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024));
|
||||
payload.addProperty("totalMemoryMb", rt.totalMemory() / (1024 * 1024));
|
||||
payload.addProperty("maxMemoryMb", rt.maxMemory() / (1024 * 1024));
|
||||
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("performance_snapshot", payload));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.FileStore;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class SystemInfoCollector {
|
||||
private static final SystemInfoCollector INSTANCE = new SystemInfoCollector();
|
||||
private static final long MB = 1024L * 1024L;
|
||||
|
||||
private final AtomicReference<String> cachedIp = new AtomicReference<>("<unknown>");
|
||||
private final AtomicReference<String> gpuRenderer = new AtomicReference<>("<pending>");
|
||||
private final AtomicReference<String> gpuVendor = new AtomicReference<>("<pending>");
|
||||
private volatile boolean gpuCollected = false;
|
||||
|
||||
private SystemInfoCollector() {
|
||||
}
|
||||
|
||||
public static SystemInfoCollector instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
fetchIpAsync();
|
||||
collectGpuOnRenderThread();
|
||||
}
|
||||
|
||||
private void fetchIpAsync() {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://api.ipify.org?format=text"))
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() == 200) {
|
||||
cachedIp.set(resp.body().trim());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void collectGpuOnRenderThread() {
|
||||
MinecraftClient.getInstance().execute(() -> {
|
||||
try {
|
||||
gpuRenderer.set(GL11.glGetString(GL11.GL_RENDERER));
|
||||
gpuVendor.set(GL11.glGetString(GL11.GL_VENDOR));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
gpuCollected = true;
|
||||
});
|
||||
}
|
||||
|
||||
public JsonObject collect() {
|
||||
JsonObject info = new JsonObject();
|
||||
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
info.addProperty("cpuCores", rt.availableProcessors());
|
||||
|
||||
try {
|
||||
var osBean = java.lang.management.ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean ext) {
|
||||
double cpuLoad = ext.getProcessCpuLoad();
|
||||
if (cpuLoad >= 0) {
|
||||
info.addProperty("cpuLoad", Math.round(cpuLoad * 100.0));
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
info.addProperty("gpuRenderer", gpuRenderer.get());
|
||||
info.addProperty("gpuVendor", gpuVendor.get());
|
||||
|
||||
info.addProperty("ramTotalMb", rt.maxMemory() / MB);
|
||||
info.addProperty("ramFreeMb", rt.freeMemory() / MB);
|
||||
info.addProperty("ramUsedMb", (rt.totalMemory() - rt.freeMemory()) / MB);
|
||||
|
||||
try {
|
||||
FileStore store = Files.getFileStore(Path.of("."));
|
||||
info.addProperty("storageTotalMb", store.getTotalSpace() / MB);
|
||||
info.addProperty("storageFreeMb", store.getUsableSpace() / MB);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
info.addProperty("publicIp", cachedIp.get());
|
||||
|
||||
try {
|
||||
info.addProperty("hostName", InetAddress.getLocalHost().getHostName());
|
||||
} catch (Exception ignored) {
|
||||
info.addProperty("hostName", System.getenv("COMPUTERNAME"));
|
||||
}
|
||||
|
||||
info.addProperty("javaVersion", System.getProperty("java.version"));
|
||||
info.addProperty("osName", System.getProperty("os.name"));
|
||||
info.addProperty("osArch", System.getProperty("os.arch"));
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public final class TelemetryCollector {
|
||||
private static final TelemetryCollector INSTANCE = new TelemetryCollector();
|
||||
private static final int MAX_QUEUE_SIZE = 1000;
|
||||
|
||||
private final ConcurrentLinkedQueue<TelemetryEvent> queue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private TelemetryCollector() {
|
||||
}
|
||||
|
||||
public static TelemetryCollector instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void record(TelemetryEvent event) {
|
||||
if (event == null) return;
|
||||
if (queue.size() >= MAX_QUEUE_SIZE) {
|
||||
queue.poll();
|
||||
}
|
||||
queue.add(event);
|
||||
}
|
||||
|
||||
public List<TelemetryEvent> drain() {
|
||||
List<TelemetryEvent> batch = new ArrayList<>();
|
||||
TelemetryEvent event;
|
||||
while ((event = queue.poll()) != null) {
|
||||
batch.add(event);
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return queue.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record TelemetryEvent(String eventType, Instant timestamp, JsonObject payload) {
|
||||
|
||||
public static TelemetryEvent now(String eventType, JsonObject payload) {
|
||||
return new TelemetryEvent(eventType, Instant.now(), payload);
|
||||
}
|
||||
|
||||
public static TelemetryEvent now(String eventType) {
|
||||
return now(eventType, new JsonObject());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import git.yawaflua.tech.spmega.ModConfig;
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class TelemetrySender {
|
||||
private static final TelemetrySender INSTANCE = new TelemetrySender();
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private final String sessionId = UUID.randomUUID().toString();
|
||||
private final HttpClient httpClient;
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
private TelemetrySender() {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TelemetrySender instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public String sessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void start(int intervalSeconds) {
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "SPMega-Telemetry-Sender");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
scheduler.scheduleAtFixedRate(this::flush, intervalSeconds, intervalSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
try {
|
||||
ModConfig config = SPMega.getConfig();
|
||||
if (config == null) return;
|
||||
|
||||
// Performance snapshot при каждом flush
|
||||
PerformanceSampler.instance().emitSnapshot();
|
||||
|
||||
// System info (only if telemetryCollectSystemInfo enabled)
|
||||
if (config.telemetryCollectSystemInfo()) {
|
||||
JsonObject sysInfo = SystemInfoCollector.instance().collect();
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("system_info", sysInfo));
|
||||
}
|
||||
|
||||
List<TelemetryEvent> events = TelemetryCollector.instance().drain();
|
||||
if (events.isEmpty()) return;
|
||||
|
||||
JsonObject batch = new JsonObject();
|
||||
batch.addProperty("clientVersion", getModVersion());
|
||||
batch.addProperty("sessionId", sessionId);
|
||||
batch.addProperty("sentAt", Instant.now().toString());
|
||||
|
||||
JsonArray eventsArray = new JsonArray();
|
||||
for (TelemetryEvent event : events) {
|
||||
JsonObject ev = new JsonObject();
|
||||
ev.addProperty("eventType", event.eventType());
|
||||
ev.addProperty("timestamp", event.timestamp().toString());
|
||||
ev.add("payload", event.payload());
|
||||
eventsArray.add(ev);
|
||||
}
|
||||
batch.add("events", eventsArray);
|
||||
|
||||
String body = GSON.toJson(batch);
|
||||
String apiDomain = config.apiDomain();
|
||||
if (apiDomain == null || apiDomain.isEmpty()) apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||
|
||||
String token = config.apiToken();
|
||||
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiDomain + "/api/v1/telemetry"))
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body));
|
||||
|
||||
if (token != null && !token.equals(ModConfig.DEFAULT_API_TOKEN)) {
|
||||
reqBuilder.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
httpClient.sendAsync(reqBuilder.build(), HttpResponse.BodyHandlers.ofString());
|
||||
} catch (Exception e) {
|
||||
System.err.println("[SPMEGA] Telemetry flush error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getModVersion() {
|
||||
try {
|
||||
return net.fabricmc.loader.api.FabricLoader.getInstance()
|
||||
.getModContainer("spmega")
|
||||
.map(c -> c.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
} catch (Exception e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package git.yawaflua.tech.spmega.client.telemetry;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class TelemetryUriSanitizer {
|
||||
private static final Pattern UUID_PATTERN = Pattern.compile(
|
||||
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile(
|
||||
"(token|key|secret|password|auth|bearer)=[^&]+", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private TelemetryUriSanitizer() {
|
||||
}
|
||||
|
||||
public static String sanitize(URI uri) {
|
||||
if (uri == null) return "<null>";
|
||||
String path = uri.getPath() != null ? uri.getPath() : "";
|
||||
path = UUID_PATTERN.matcher(path).replaceAll("<uuid>");
|
||||
|
||||
String query = uri.getQuery();
|
||||
if (query != null) {
|
||||
query = TOKEN_PATTERN.matcher(query).replaceAll("$1=<redacted>");
|
||||
return uri.getHost() + path + "?" + query;
|
||||
}
|
||||
return uri.getHost() + path;
|
||||
}
|
||||
|
||||
public static String sanitize(String url) {
|
||||
try {
|
||||
return sanitize(URI.create(url));
|
||||
} catch (Exception e) {
|
||||
return "<invalid-url>";
|
||||
}
|
||||
}
|
||||
|
||||
public static String classifyTarget(URI uri) {
|
||||
if (uri == null) return "unknown";
|
||||
String host = uri.getHost();
|
||||
if (host == null) return "unknown";
|
||||
if (host.contains("spworlds.ru")) return "spworlds";
|
||||
if (host.contains("spmega") || host.contains("ywfl.dev") || host.contains("yawaflua")) return "spmega-backend";
|
||||
if (host.contains("mojang.com") || host.contains("minecraft.net")) return "mojang";
|
||||
if (host.contains("sp-mini.ru") || host.contains("spmap")) return "gps-map";
|
||||
if (host.contains("ipify.org")) return "ipify";
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package git.yawaflua.tech.spmega.client.ui;
|
||||
import com.google.gson.Gson;
|
||||
import git.yawaflua.tech.spmega.GpsHudPosition;
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.InstrumentedHttpClient;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
@@ -10,9 +11,7 @@ import net.minecraft.text.Text;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -52,13 +51,12 @@ public final class GpsHudRenderer {
|
||||
|
||||
private void fetchCities() {
|
||||
try {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient client = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://map.sp-mini.ru/api/map/territories"))
|
||||
// Не понимаю смысла менять с домена со своим именем АКА реклама личного бренда на безликий сп-мини.ру, пахнет дешевой подделкой
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
var response = client.send(request);
|
||||
if (response.statusCode() == 200) {
|
||||
TerritoryWrapper[] parsed = new Gson().fromJson(response.body(), TerritoryWrapper[].class);
|
||||
if (parsed != null) {
|
||||
|
||||
+19
-16
@@ -7,12 +7,12 @@ import com.mojang.authlib.exceptions.AuthenticationException;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
import git.yawaflua.tech.spmega.ModConfig;
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.InstrumentedHttpClient;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.session.Session;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
@@ -71,14 +71,14 @@ public class BackendAuthenticator {
|
||||
|
||||
String url = apiDomain + "/api/v1/auth/start";
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.method("POST", HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
if (response.statusCode() != 200) {
|
||||
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on start: " + response.body());
|
||||
throw new IOException("Server returned status code " + response.statusCode() + " on start: " + response.body());
|
||||
@@ -109,14 +109,14 @@ public class BackendAuthenticator {
|
||||
|
||||
String url = apiDomain + "/api/v1/auth/validate";
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
if (response.statusCode() != 200) {
|
||||
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
||||
throw new IOException("Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
||||
@@ -139,7 +139,10 @@ public class BackendAuthenticator {
|
||||
config.allowBackend(),
|
||||
config.signQuickPayEnabled(),
|
||||
config.gpsEnabled(),
|
||||
config.gpsPosition()
|
||||
config.gpsPosition(),
|
||||
config.telemetryEnabled(),
|
||||
config.telemetryIntervalSeconds(),
|
||||
config.telemetryCollectSystemInfo()
|
||||
);
|
||||
SPMega.setConfig(updated);
|
||||
System.out.println("[SPMEGA] Backend auth successful, saved token.");
|
||||
@@ -167,7 +170,7 @@ public class BackendAuthenticator {
|
||||
jsonPayload.addProperty("token", cardToken);
|
||||
String requestBody = jsonPayload.toString();
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
@@ -175,7 +178,7 @@ public class BackendAuthenticator {
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
httpClient.sendAsync(request)
|
||||
.thenAccept(response -> {
|
||||
if (response.statusCode() != 200 && response.statusCode() != 204) {
|
||||
System.err.println("[SPMEGA] Failed to send card to backend: status code "
|
||||
@@ -206,14 +209,14 @@ public class BackendAuthenticator {
|
||||
|
||||
String url = apiDomain + "/api/v1/auth/cards";
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + config.apiToken())
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Server returned status code " + response.statusCode() + " on fetch cards.");
|
||||
}
|
||||
@@ -253,14 +256,14 @@ public class BackendAuthenticator {
|
||||
|
||||
String url = apiDomain + "/api/v1/auth/cards/" + cardId;
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + config.apiToken())
|
||||
.DELETE()
|
||||
.build();
|
||||
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
httpClient.sendAsync(request)
|
||||
.thenAccept(response -> {
|
||||
if (response.statusCode() != 200 && response.statusCode() != 204) {
|
||||
System.err.println("[SPMEGA] Failed to delete card from backend: status code "
|
||||
@@ -300,7 +303,7 @@ public class BackendAuthenticator {
|
||||
jsonPayload.addProperty("comment", comment);
|
||||
String requestBody = jsonPayload.toString();
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
@@ -308,7 +311,7 @@ public class BackendAuthenticator {
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
if (response.statusCode() != 200 && response.statusCode() != 201 && response.statusCode() != 204) {
|
||||
throw new IOException("Server returned status code " + response.statusCode() + ": " + response.body());
|
||||
}
|
||||
@@ -334,14 +337,14 @@ public class BackendAuthenticator {
|
||||
String url = apiDomain + "/api/v1/transactions?p=" + page;
|
||||
System.out.println("[SPMEGA] Requesting transactions from URL: " + url);
|
||||
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
InstrumentedHttpClient httpClient = new InstrumentedHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer " + config.apiToken())
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
HttpResponse<String> response = httpClient.send(request);
|
||||
System.out.println("[SPMEGA] Response status code: " + response.statusCode());
|
||||
if (response.statusCode() != 200) {
|
||||
System.err.println("[SPMEGA] Failed response body: " + response.body());
|
||||
|
||||
@@ -2,7 +2,9 @@ package git.yawaflua.tech.spmega.client.ui.service;
|
||||
|
||||
import git.yawaflua.tech.spmega.ModConfig;
|
||||
import git.yawaflua.tech.spmega.SPMega;
|
||||
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
|
||||
import git.yawaflua.tech.spmega.client.api.SPWorldsApiClient;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.TelemetryCollector;
|
||||
import git.yawaflua.tech.spmega.client.telemetry.TelemetryEvent;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
@@ -248,12 +250,17 @@ public final class BankUiService {
|
||||
CardCredentials credentials = database.getCredentials(draft.senderCardId());
|
||||
if (credentials == null) {
|
||||
lastMessage = "Не найдены креды карты отправителя";
|
||||
recordPaymentEvent(draft, false, "local", "no_credentials", 0, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
||||
boolean useBackend = config != null && config.allowBackend();
|
||||
String mode = useBackend ? "backend" : "spworlds-direct";
|
||||
String destination = useBackend ? "backend" : "local-db";
|
||||
String senderLast4 = extractLastDigits(credentials.cardId());
|
||||
|
||||
long startNs = System.nanoTime();
|
||||
try {
|
||||
long newBalance = 0;
|
||||
if (useBackend) {
|
||||
@@ -297,6 +304,8 @@ public final class BankUiService {
|
||||
|
||||
reloadCardsFromDb();
|
||||
lastMessage = "Перевод выполнен";
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
recordPaymentEvent(draft, true, mode, destination, durationMs, senderLast4);
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
database.insertTransferHistory(
|
||||
@@ -308,11 +317,32 @@ public final class BankUiService {
|
||||
"FAILED: " + trimMessage(exception.getMessage())
|
||||
);
|
||||
lastMessage = "Ошибка перевода: " + exception.getMessage();
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
recordPaymentEvent(draft, false, mode, destination, durationMs, senderLast4, exception.getMessage());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void recordPaymentEvent(PaymentDraft draft, boolean success, String mode, String destination, long durationMs, String senderLast4) {
|
||||
recordPaymentEvent(draft, success, mode, destination, durationMs, senderLast4, null);
|
||||
}
|
||||
|
||||
private void recordPaymentEvent(PaymentDraft draft, boolean success, String mode, String destination, long durationMs, String senderLast4, String error) {
|
||||
com.google.gson.JsonObject payload = new com.google.gson.JsonObject();
|
||||
payload.addProperty("success", success);
|
||||
payload.addProperty("mode", mode);
|
||||
payload.addProperty("destination", destination);
|
||||
payload.addProperty("durationMs", durationMs);
|
||||
payload.addProperty("senderCardLast4", senderLast4 != null ? senderLast4 : "unknown");
|
||||
payload.addProperty("amount", draft.amount());
|
||||
payload.addProperty("receiver", draft.recipient());
|
||||
if (error != null) {
|
||||
payload.addProperty("error", trimMessage(error));
|
||||
}
|
||||
TelemetryCollector.instance().record(TelemetryEvent.now("payment", payload));
|
||||
}
|
||||
|
||||
private boolean refreshCardSync(
|
||||
String cardId,
|
||||
String cardToken,
|
||||
|
||||
@@ -28,7 +28,14 @@
|
||||
"option.spmega.gps_position.bottom_left": "Bottom-Left",
|
||||
"option.spmega.gps_position.bottom_right": "Bottom-Right",
|
||||
"option.spmega.gps_position.bottom_center": "Bottom-center",
|
||||
"option.spmega.gps_position.top_center": "Top-center"
|
||||
"option.spmega.gps_position.top_center": "Top-center",
|
||||
"category.spmega.telemetry": "Telemetry",
|
||||
"option.spmega.telemetry_enabled": "Collect system information",
|
||||
"tooltip.spmega.telemetry_enabled": "Disabling hides hardware data (CPU, GPU, RAM, IP). Performance and request stats keep sending.",
|
||||
"option.spmega.telemetry_interval": "Send interval (sec)",
|
||||
"tooltip.spmega.telemetry_interval": "How often telemetry batches are sent to the server (10-600 sec)",
|
||||
"option.spmega.telemetry_system_info": "Send system information",
|
||||
"tooltip.spmega.telemetry_system_info": "CPU, GPU, RAM, storage, IP, device name, Java version"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
"option.spmega.gps_position.bottom_left": "Слева внизу",
|
||||
"option.spmega.gps_position.bottom_center": "Снизу по-центру",
|
||||
"option.spmega.gps_position.top_center": "Сверху по-центру",
|
||||
"option.spmega.gps_position.bottom_right": "Снизу справа"
|
||||
"option.spmega.gps_position.bottom_right": "Снизу справа",
|
||||
"category.spmega.telemetry": "Телеметрия",
|
||||
"option.spmega.telemetry_enabled": "Сбор системной информации",
|
||||
"tooltip.spmega.telemetry_enabled": "Отключение скрывает данные компьютера (CPU, GPU, RAM, IP). Статистика производительности и запросов продолжает отправляться.",
|
||||
"option.spmega.telemetry_interval": "Интервал отправки (сек)",
|
||||
"tooltip.spmega.telemetry_interval": "Как часто батч телеметрии отправляется на сервер (от 10 до 600 сек)",
|
||||
"option.spmega.telemetry_system_info": "Отправлять информацию о системе",
|
||||
"tooltip.spmega.telemetry_system_info": "CPU, GPU, RAM, диск, IP, имя устройства, версия Java"
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,26 @@ public final class ConfigManager {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition);
|
||||
boolean telemetryEnabled = readBoolean(properties, "telemetry.enabled", defaults.telemetryEnabled());
|
||||
String rawTelemetry = properties.getProperty("telemetry.enabled");
|
||||
if (rawTelemetry == null || !Boolean.toString(telemetryEnabled).equalsIgnoreCase(rawTelemetry.trim())) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
int telemetryIntervalSeconds = readInt(properties, "telemetry.intervalSeconds", defaults.telemetryIntervalSeconds());
|
||||
String rawInterval = properties.getProperty("telemetry.intervalSeconds");
|
||||
if (rawInterval == null || !Integer.toString(telemetryIntervalSeconds).equals(rawInterval.trim())) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
boolean telemetryCollectSystemInfo = readBoolean(properties, "telemetry.collectSystemInfo", defaults.telemetryCollectSystemInfo());
|
||||
String rawSysInfo = properties.getProperty("telemetry.collectSystemInfo");
|
||||
if (rawSysInfo == null || !Boolean.toString(telemetryCollectSystemInfo).equalsIgnoreCase(rawSysInfo.trim())) {
|
||||
shouldSave = true;
|
||||
}
|
||||
|
||||
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition,
|
||||
telemetryEnabled, telemetryIntervalSeconds, telemetryCollectSystemInfo);
|
||||
|
||||
|
||||
if (shouldSave) {
|
||||
@@ -129,6 +148,9 @@ public final class ConfigManager {
|
||||
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
||||
properties.setProperty("gps.enabled", Boolean.toString(config.gpsEnabled()));
|
||||
properties.setProperty("gps.position", config.gpsPosition().name());
|
||||
properties.setProperty("telemetry.enabled", Boolean.toString(config.telemetryEnabled()));
|
||||
properties.setProperty("telemetry.intervalSeconds", Integer.toString(config.telemetryIntervalSeconds()));
|
||||
properties.setProperty("telemetry.collectSystemInfo", Boolean.toString(config.telemetryCollectSystemInfo()));
|
||||
|
||||
try {
|
||||
Files.createDirectories(configPath.getParent());
|
||||
@@ -140,6 +162,18 @@ public final class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static int readInt(Properties properties, String key, int fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static <E extends Enum<E>> E readEnum(Properties properties, String key, Class<E> enumClass, E fallback) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package git.yawaflua.tech.spmega;
|
||||
|
||||
public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled,
|
||||
boolean gpsEnabled, GpsHudPosition gpsPosition) {
|
||||
boolean gpsEnabled, GpsHudPosition gpsPosition,
|
||||
boolean telemetryEnabled, int telemetryIntervalSeconds, boolean telemetryCollectSystemInfo) {
|
||||
public static final String DEFAULT_API_DOMAIN = "https://spmega.yawaflua.tech";
|
||||
public static final boolean ALLOW_BACKEND = true;
|
||||
public static final String DEFAULT_API_TOKEN = "-";
|
||||
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
||||
public static final boolean DEFAULT_GPS_ENABLED = true;
|
||||
public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_CENTER;
|
||||
public static final boolean DEFAULT_TELEMETRY_ENABLED = true;
|
||||
public static final int DEFAULT_TELEMETRY_INTERVAL_SECONDS = 60;
|
||||
public static final boolean DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO = true;
|
||||
|
||||
public static ModConfig createDefault() {
|
||||
return new ModConfig(
|
||||
@@ -16,9 +20,10 @@ public record ModConfig(String apiDomain, String apiToken, boolean allowBackend,
|
||||
ALLOW_BACKEND,
|
||||
DEFAULT_SIGN_QUICK_PAY_ENABLED,
|
||||
DEFAULT_GPS_ENABLED,
|
||||
DEFAULT_GPS_POSITION
|
||||
DEFAULT_GPS_POSITION,
|
||||
DEFAULT_TELEMETRY_ENABLED,
|
||||
DEFAULT_TELEMETRY_INTERVAL_SECONDS,
|
||||
DEFAULT_TELEMETRY_COLLECT_SYSTEM_INFO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ public class SPMega implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
long startNs = System.nanoTime();
|
||||
config = ConfigManager.loadOrCreate();
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
System.out.println("[SPMEGA] Main init took " + durationMs + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user