removed logrocket and fixed Drawing UX
This commit is contained in:
@@ -11,4 +11,11 @@ public interface IDrawingCatalogController
|
||||
event Action ListChanged;
|
||||
UniTask InitializeAsync(CancellationToken ct);
|
||||
void OnTemplateSelected(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Signalled by the view layer once the catalog has finished populating after a
|
||||
/// <see cref="ListChanged"/> refresh. Lets <see cref="InitializeAsync"/> keep the loading
|
||||
/// screen up until items are on screen, instead of revealing an empty catalog that fills in later.
|
||||
/// </summary>
|
||||
void NotifyPopulated();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddc06781290bc42b480134ac73d6a269
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Services.LogRocket
|
||||
{
|
||||
public interface ILogRocketService
|
||||
{
|
||||
bool IsReady { get; }
|
||||
|
||||
void Identify(string userId);
|
||||
void Identify(string userId, IReadOnlyDictionary<string, object> traits);
|
||||
|
||||
void Track(string eventName);
|
||||
void Track(string eventName, string propName, string propValue);
|
||||
void Track(string eventName, IReadOnlyDictionary<string, object> properties);
|
||||
|
||||
void Log(string message, LogRocketSeverity severity = LogRocketSeverity.Info);
|
||||
void LogException(string message, string stackTrace);
|
||||
|
||||
void StartSessionReplay();
|
||||
void StopSessionReplay();
|
||||
|
||||
string SessionUrl { get; }
|
||||
}
|
||||
|
||||
public enum LogRocketSeverity
|
||||
{
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a44c903b7fed4e889c9fc91430e4b71
|
||||
@@ -17,6 +17,8 @@ public sealed class DrawingCatalogController : IDrawingCatalogController
|
||||
public IReadOnlyList<string> VisibleIds => _visible;
|
||||
public event Action ListChanged;
|
||||
|
||||
private UniTaskCompletionSource _firstPopulate;
|
||||
|
||||
public DrawingCatalogController(
|
||||
IDrawingTemplateCatalog catalog,
|
||||
IEventBus bus)
|
||||
@@ -28,9 +30,25 @@ public sealed class DrawingCatalogController : IDrawingCatalogController
|
||||
public async UniTask InitializeAsync(CancellationToken ct)
|
||||
{
|
||||
await _catalog.FetchAsync();
|
||||
|
||||
// No view listening (e.g. catalog view unassigned) — nothing will populate, so don't wait.
|
||||
if (ListChanged == null)
|
||||
{
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hold here until the presenter reports the catalog is on screen, so the caller can keep
|
||||
// the loading screen up across the (async) thumbnail load + button spawn instead of
|
||||
// revealing an empty catalog that fills in a few frames later.
|
||||
_firstPopulate = new UniTaskCompletionSource();
|
||||
Refresh();
|
||||
using (ct.Register(() => _firstPopulate.TrySetResult()))
|
||||
await _firstPopulate.Task;
|
||||
}
|
||||
|
||||
public void NotifyPopulated() => _firstPopulate?.TrySetResult();
|
||||
|
||||
public void OnTemplateSelected(string id)
|
||||
{
|
||||
_bus.Publish(new DrawingSelectedSignal(id));
|
||||
|
||||
@@ -120,6 +120,8 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
_view.SetItems(vms);
|
||||
_view.SetPagination(_currentPage, _totalPages);
|
||||
|
||||
// Unblock InitializeAsync: items are now on screen, so the loading screen can hide.
|
||||
_controller.NotifyPopulated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6df39d33cdd84ce1ab71ee4e5026682
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8eef8a073e91748a09d25cf48e413e05
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,32 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the LogRocket service.
|
||||
///
|
||||
/// Session replay is recorded natively and automatically by the LogRocket mobile SDK
|
||||
/// from init - there is no per-frame upload from Unity. Use
|
||||
/// <see cref="Core.Contracts.Services.LogRocket.ILogRocketService"/>'s
|
||||
/// StartSessionReplay / StopSessionReplay for manual session control. NOTE: native
|
||||
/// capture may not see Unity's Metal/GL surface - verify replay content on a device.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Darkmatter/Services/LogRocket Config", fileName = "LogRocketConfig")]
|
||||
public sealed class LogRocketConfig : ScriptableObject
|
||||
{
|
||||
[Header("Project")]
|
||||
[Tooltip("LogRocket application ID, e.g. \"yourorg/yourapp\".")]
|
||||
public string AppId;
|
||||
|
||||
[Header("Logs")]
|
||||
[Tooltip("Forward Unity Debug.Log / exceptions to LogRocket.")]
|
||||
public bool ForwardUnityLogs = true;
|
||||
|
||||
[Tooltip("Minimum Unity log type forwarded.")]
|
||||
public LogType MinimumLogType = LogType.Warning;
|
||||
|
||||
[Header("Behaviour")]
|
||||
[Tooltip("Disable LogRocket entirely in the editor.")]
|
||||
public bool DisableInEditor = true;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e20e49adf823a4918b4b7823d203a28b
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7656001fcf7748289669ff7f1bba786
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
External Dependency Manager (EDM4U) dependency spec for the LogRocket SDK.
|
||||
EDM injects these into the generated iOS Podfile / Android Gradle on build.
|
||||
|
||||
Android: VERIFIED against the actual com.logrocket:logrocket:3.1.0 AAR bytecode
|
||||
(javap). The com.logrocket.core.LogRocket class exists only in the 3.x line - the
|
||||
2.x line ships com.logrocket.core.SDK instead - so the range MUST stay >= 3.0,
|
||||
or the direct-call bridge will not compile/link. Latest at time of writing: 3.1.0.
|
||||
|
||||
iOS: NOT a pod. The LogRocket.xcframework 3.1.0 (a dynamic Swift framework) is
|
||||
vendored directly at Assets/Plugins/iOS/LogRocket.xcframework. The CocoaPods
|
||||
integration linked it but did not embed it into the .app, causing a launch-time
|
||||
dyld failure (@rpath/LogRocket.framework/LogRocket not loaded). Vendoring routes
|
||||
it through Unity's plugin pipeline, which auto-embeds + signs dynamic frameworks.
|
||||
The .m bridge (verified against this exact 3.1.0 ObjC header) is unchanged.
|
||||
|
||||
NOTE: the Android bridge (Plugins/Android/LogRocketUnityBridge.java) calls the
|
||||
SDK directly, so the androidPackage below is REQUIRED - removing it will break
|
||||
the Android compile.
|
||||
-->
|
||||
<dependencies>
|
||||
<androidPackages>
|
||||
<androidPackage spec="com.logrocket:logrocket:[3.0,4.0)">
|
||||
<repositories>
|
||||
<repository>https://storage.googleapis.com/logrocket-maven/</repository>
|
||||
</repositories>
|
||||
</androidPackage>
|
||||
</androidPackages>
|
||||
</dependencies>
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2806d88a0ebc4aaeb06d2fd098c7555
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d3c637029058429a8ea92a186cba06f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,28 +0,0 @@
|
||||
using Darkmatter.Core.Contracts.Services.LogRocket;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using Darkmatter.Services.LogRocket.Configuration;
|
||||
using Darkmatter.Services.LogRocket.Systems;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Installers
|
||||
{
|
||||
public sealed class LogRocketModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private LogRocketConfig _config;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
var config = _config;
|
||||
if (config == null)
|
||||
{
|
||||
Debug.LogWarning("[LogRocket] LogRocketModule has no config assigned. Service will be disabled.");
|
||||
config = ScriptableObject.CreateInstance<LogRocketConfig>();
|
||||
}
|
||||
|
||||
builder.RegisterInstance(config);
|
||||
builder.RegisterEntryPoint<LogRocketSystem>().As<ILogRocketService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f78e90a5f08704f62b3624ff47ebf66c
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "Services.LogRocket",
|
||||
"rootNamespace": "Darkmatter.Services.LogRocket",
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a1957cf2cabd413bad46969b408df0e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 491835b65b47144db979541bb7e37499
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
internal interface ILogRocketNative
|
||||
{
|
||||
bool IsAvailable { get; }
|
||||
string SessionUrl { get; }
|
||||
void Init(string appId);
|
||||
void Identify(string userId, IReadOnlyDictionary<string, object> traits);
|
||||
void Track(string eventName, IReadOnlyDictionary<string, object> properties);
|
||||
void Log(string severity, string message);
|
||||
void LogException(string message, string stackTrace);
|
||||
void StartReplay();
|
||||
void StopReplay();
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14484983d15d0451ebe505bb8f93f447
|
||||
@@ -1,135 +0,0 @@
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
internal sealed class LogRocketNativeAndroid : ILogRocketNative
|
||||
{
|
||||
private const string BridgeClass = "com.darkmatter.logrocket.LogRocketUnityBridge";
|
||||
|
||||
// The Java bridge exposes static methods (Plugins/Android/LogRocketUnityBridge.java).
|
||||
private AndroidJavaClass _bridge;
|
||||
public bool IsAvailable => _bridge != null;
|
||||
public string SessionUrl => SafeCall<string>("getSessionUrl");
|
||||
|
||||
public void Init(string appId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
|
||||
using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
|
||||
_bridge = new AndroidJavaClass(BridgeClass);
|
||||
_bridge.CallStatic("init", activity, appId ?? string.Empty);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[LogRocket] Android init failed: {e}");
|
||||
_bridge = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Identify(string userId, IReadOnlyDictionary<string, object> traits)
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("identify", userId ?? string.Empty, MapToJson(traits)); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] identify failed: {e}"); }
|
||||
}
|
||||
|
||||
public void Track(string eventName, IReadOnlyDictionary<string, object> properties)
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("track", eventName ?? string.Empty, MapToJson(properties)); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] track failed: {e}"); }
|
||||
}
|
||||
|
||||
public void Log(string severity, string message)
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("log", severity ?? "info", message ?? string.Empty); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] log failed: {e}"); }
|
||||
}
|
||||
|
||||
public void LogException(string message, string stackTrace)
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("logException", message ?? string.Empty, stackTrace ?? string.Empty); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] logException failed: {e}"); }
|
||||
}
|
||||
|
||||
public void StartReplay()
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("startReplay"); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] startReplay failed: {e}"); }
|
||||
}
|
||||
|
||||
public void StopReplay()
|
||||
{
|
||||
if (_bridge == null) return;
|
||||
try { _bridge.CallStatic("stopReplay"); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] stopReplay failed: {e}"); }
|
||||
}
|
||||
|
||||
private T SafeCall<T>(string method)
|
||||
{
|
||||
if (_bridge == null) return default;
|
||||
try { return _bridge.CallStatic<T>(method); }
|
||||
catch { return default; }
|
||||
}
|
||||
|
||||
private static string MapToJson(IReadOnlyDictionary<string, object> map)
|
||||
{
|
||||
if (map == null || map.Count == 0) return "{}";
|
||||
var sb = new System.Text.StringBuilder(64);
|
||||
sb.Append('{');
|
||||
bool first = true;
|
||||
foreach (var kv in map)
|
||||
{
|
||||
if (!first) sb.Append(',');
|
||||
first = false;
|
||||
AppendJsonString(sb, kv.Key);
|
||||
sb.Append(':');
|
||||
AppendJsonValue(sb, kv.Value);
|
||||
}
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendJsonValue(System.Text.StringBuilder sb, object v)
|
||||
{
|
||||
switch (v)
|
||||
{
|
||||
case null: sb.Append("null"); break;
|
||||
case bool b: sb.Append(b ? "true" : "false"); break;
|
||||
case int or long or float or double or decimal:
|
||||
sb.Append(Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture));
|
||||
break;
|
||||
default: AppendJsonString(sb, v.ToString()); break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendJsonString(System.Text.StringBuilder sb, string s)
|
||||
{
|
||||
sb.Append('"');
|
||||
foreach (var c in s ?? string.Empty)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) sb.AppendFormat("\\u{0:X4}", (int)c);
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
sb.Append('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e384b24344c8e4899be0036dd9edc333
|
||||
@@ -1,135 +0,0 @@
|
||||
#if UNITY_IOS && !UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
internal sealed class LogRocketNativeIOS : ILogRocketNative
|
||||
{
|
||||
[DllImport("__Internal")] private static extern void _lr_init(string appId);
|
||||
[DllImport("__Internal")] private static extern void _lr_identify(string userId, string traitsJson);
|
||||
[DllImport("__Internal")] private static extern void _lr_track(string eventName, string propsJson);
|
||||
[DllImport("__Internal")] private static extern void _lr_log(string severity, string message);
|
||||
[DllImport("__Internal")] private static extern void _lr_logException(string message, string stack);
|
||||
[DllImport("__Internal")] private static extern void _lr_startReplay();
|
||||
[DllImport("__Internal")] private static extern void _lr_stopReplay();
|
||||
[DllImport("__Internal")] private static extern IntPtr _lr_sessionUrl();
|
||||
|
||||
private bool _initialised;
|
||||
public bool IsAvailable => _initialised;
|
||||
|
||||
public string SessionUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var ptr = _lr_sessionUrl();
|
||||
return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAuto(ptr);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(string appId)
|
||||
{
|
||||
try { _lr_init(appId ?? string.Empty); _initialised = true; }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] iOS init failed: {e}"); _initialised = false; }
|
||||
}
|
||||
|
||||
public void Identify(string userId, IReadOnlyDictionary<string, object> traits)
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_identify(userId ?? string.Empty, MapToJson(traits)); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] identify failed: {e}"); }
|
||||
}
|
||||
|
||||
public void Track(string eventName, IReadOnlyDictionary<string, object> properties)
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_track(eventName ?? string.Empty, MapToJson(properties)); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] track failed: {e}"); }
|
||||
}
|
||||
|
||||
public void Log(string severity, string message)
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_log(severity ?? "info", message ?? string.Empty); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] log failed: {e}"); }
|
||||
}
|
||||
|
||||
public void LogException(string message, string stack)
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_logException(message ?? string.Empty, stack ?? string.Empty); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] logException failed: {e}"); }
|
||||
}
|
||||
|
||||
public void StartReplay()
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_startReplay(); } catch (Exception e) { Debug.LogError($"[LogRocket] startReplay failed: {e}"); }
|
||||
}
|
||||
|
||||
public void StopReplay()
|
||||
{
|
||||
if (!_initialised) return;
|
||||
try { _lr_stopReplay(); } catch (Exception e) { Debug.LogError($"[LogRocket] stopReplay failed: {e}"); }
|
||||
}
|
||||
|
||||
private static string MapToJson(IReadOnlyDictionary<string, object> map)
|
||||
{
|
||||
if (map == null || map.Count == 0) return "{}";
|
||||
var sb = new System.Text.StringBuilder(64);
|
||||
sb.Append('{');
|
||||
bool first = true;
|
||||
foreach (var kv in map)
|
||||
{
|
||||
if (!first) sb.Append(',');
|
||||
first = false;
|
||||
AppendJsonString(sb, kv.Key);
|
||||
sb.Append(':');
|
||||
AppendJsonValue(sb, kv.Value);
|
||||
}
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendJsonValue(System.Text.StringBuilder sb, object v)
|
||||
{
|
||||
switch (v)
|
||||
{
|
||||
case null: sb.Append("null"); break;
|
||||
case bool b: sb.Append(b ? "true" : "false"); break;
|
||||
case int or long or float or double or decimal:
|
||||
sb.Append(Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture));
|
||||
break;
|
||||
default: AppendJsonString(sb, v.ToString()); break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendJsonString(System.Text.StringBuilder sb, string s)
|
||||
{
|
||||
sb.Append('"');
|
||||
foreach (var c in s ?? string.Empty)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) sb.AppendFormat("\\u{0:X4}", (int)c);
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
sb.Append('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd81dfea0269d4dec8e97e8756b80801
|
||||
@@ -1,33 +0,0 @@
|
||||
#if UNITY_EDITOR || (!UNITY_IOS && !UNITY_ANDROID)
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
internal sealed class LogRocketNativeNoop : ILogRocketNative
|
||||
{
|
||||
public bool IsAvailable => true;
|
||||
public string SessionUrl => null;
|
||||
|
||||
public void Init(string appId) =>
|
||||
Debug.Log($"[LogRocket] Editor/standalone stub init (appId='{appId}'). No native upload.");
|
||||
|
||||
public void Identify(string userId, IReadOnlyDictionary<string, object> traits) =>
|
||||
Debug.Log($"[LogRocket] Identify '{userId}' traits={Count(traits)}");
|
||||
|
||||
public void Track(string eventName, IReadOnlyDictionary<string, object> properties) =>
|
||||
Debug.Log($"[LogRocket] Track '{eventName}' props={Count(properties)}");
|
||||
|
||||
public void Log(string severity, string message) =>
|
||||
Debug.Log($"[LogRocket:{severity}] {message}");
|
||||
|
||||
public void LogException(string message, string stackTrace) =>
|
||||
Debug.Log($"[LogRocket:exception] {message}\n{stackTrace}");
|
||||
|
||||
public void StartReplay() => Debug.Log("[LogRocket] StartReplay (noop)");
|
||||
public void StopReplay() => Debug.Log("[LogRocket] StopReplay (noop)");
|
||||
|
||||
private static int Count(IReadOnlyDictionary<string, object> map) => map?.Count ?? 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 545348daa325e4c809265a9f76584f19
|
||||
@@ -1,153 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Services.LogRocket;
|
||||
using Darkmatter.Services.LogRocket.Configuration;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
public sealed class LogRocketSystem : ILogRocketService, IAsyncStartable, IDisposable
|
||||
{
|
||||
private readonly LogRocketConfig _config;
|
||||
private readonly ILogRocketNative _native;
|
||||
private readonly Queue<Action> _pending = new();
|
||||
|
||||
private UnityLogForwarder _logForwarder;
|
||||
private bool _ready;
|
||||
private bool _disabled;
|
||||
|
||||
public LogRocketSystem(LogRocketConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_native = CreateNative();
|
||||
}
|
||||
|
||||
public bool IsReady => _ready;
|
||||
public string SessionUrl => _native?.SessionUrl;
|
||||
|
||||
public async UniTask StartAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
Debug.LogWarning("[LogRocket] No config assigned. Service disabled.");
|
||||
_disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (_config.DisableInEditor) { _disabled = true; return; }
|
||||
#endif
|
||||
if (string.IsNullOrWhiteSpace(_config.AppId))
|
||||
{
|
||||
Debug.LogWarning("[LogRocket] AppId empty. Service disabled.");
|
||||
_disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await UniTask.SwitchToMainThread(cancellation);
|
||||
_native.Init(_config.AppId);
|
||||
|
||||
if (_config.ForwardUnityLogs)
|
||||
{
|
||||
_logForwarder = new UnityLogForwarder(_native, _config.MinimumLogType);
|
||||
_logForwarder.Enable();
|
||||
}
|
||||
|
||||
// Session replay records natively from init - no Unity-side start needed.
|
||||
|
||||
_ready = true;
|
||||
FlushPending();
|
||||
}
|
||||
|
||||
public void Identify(string userId) => Identify(userId, null);
|
||||
|
||||
public void Identify(string userId, IReadOnlyDictionary<string, object> traits)
|
||||
{
|
||||
Run(() => _native.Identify(userId, traits));
|
||||
}
|
||||
|
||||
public void Track(string eventName)
|
||||
{
|
||||
Run(() => _native.Track(eventName, null));
|
||||
}
|
||||
|
||||
public void Track(string eventName, string propName, string propValue)
|
||||
{
|
||||
var dict = new Dictionary<string, object>(1) { [propName] = propValue };
|
||||
Run(() => _native.Track(eventName, dict));
|
||||
}
|
||||
|
||||
public void Track(string eventName, IReadOnlyDictionary<string, object> properties)
|
||||
{
|
||||
Run(() => _native.Track(eventName, properties));
|
||||
}
|
||||
|
||||
public void Log(string message, LogRocketSeverity severity = LogRocketSeverity.Info)
|
||||
{
|
||||
Run(() => _native.Log(SeverityString(severity), message));
|
||||
}
|
||||
|
||||
public void LogException(string message, string stackTrace)
|
||||
{
|
||||
Run(() => _native.LogException(message, stackTrace));
|
||||
}
|
||||
|
||||
// Manual session control. Recording is automatic from init; these map to the
|
||||
// native SDK's session controls (e.g. iOS startNewSession / shutdown). Useful
|
||||
// to rotate or halt a recording, e.g. around a sensitive screen.
|
||||
public void StartSessionReplay() => Run(() => _native.StartReplay());
|
||||
|
||||
public void StopSessionReplay() => Run(() => _native.StopReplay());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logForwarder?.Disable();
|
||||
_logForwarder = null;
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
private void Run(Action action)
|
||||
{
|
||||
if (_disabled) return;
|
||||
if (_ready)
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] op failed: {e}"); }
|
||||
}
|
||||
else _pending.Enqueue(action);
|
||||
}
|
||||
|
||||
private void FlushPending()
|
||||
{
|
||||
while (_pending.Count > 0)
|
||||
{
|
||||
try { _pending.Dequeue().Invoke(); }
|
||||
catch (Exception e) { Debug.LogError($"[LogRocket] queued op failed: {e}"); }
|
||||
}
|
||||
}
|
||||
|
||||
private static string SeverityString(LogRocketSeverity s) => s switch
|
||||
{
|
||||
LogRocketSeverity.Debug => "debug",
|
||||
LogRocketSeverity.Warn => "warn",
|
||||
LogRocketSeverity.Error => "error",
|
||||
_ => "info",
|
||||
};
|
||||
|
||||
private static ILogRocketNative CreateNative()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
return new LogRocketNativeNoop();
|
||||
#elif UNITY_ANDROID
|
||||
return new LogRocketNativeAndroid();
|
||||
#elif UNITY_IOS
|
||||
return new LogRocketNativeIOS();
|
||||
#else
|
||||
return new LogRocketNativeNoop();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5c8a2c56a83847a3b480e4a13fba63d
|
||||
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.LogRocket.Systems
|
||||
{
|
||||
internal sealed class UnityLogForwarder
|
||||
{
|
||||
// Prefix every LogRocket diagnostic carries. Used to drop our own chatter so it
|
||||
// never round-trips back into the session as user telemetry.
|
||||
private const string SelfPrefix = "[LogRocket";
|
||||
|
||||
private readonly ILogRocketNative _native;
|
||||
private readonly LogType _min;
|
||||
private bool _enabled;
|
||||
|
||||
// Guards against a native Log/LogException call that itself emits a Unity log on the
|
||||
// same thread, which would otherwise recurse through OnLog.
|
||||
[ThreadStatic] private static bool _dispatching;
|
||||
|
||||
public UnityLogForwarder(ILogRocketNative native, LogType min)
|
||||
{
|
||||
_native = native;
|
||||
_min = min;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if (_enabled) return;
|
||||
Application.logMessageReceivedThreaded += OnLog;
|
||||
_enabled = true;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (!_enabled) return;
|
||||
Application.logMessageReceivedThreaded -= OnLog;
|
||||
_enabled = false;
|
||||
}
|
||||
|
||||
private void OnLog(string condition, string stack, LogType type)
|
||||
{
|
||||
if (_dispatching) return;
|
||||
if (!ShouldForward(type)) return;
|
||||
if (condition != null && condition.StartsWith(SelfPrefix, StringComparison.Ordinal)) return;
|
||||
|
||||
_dispatching = true;
|
||||
try
|
||||
{
|
||||
if (type == LogType.Exception)
|
||||
_native.LogException(condition, stack);
|
||||
else
|
||||
_native.Log(Severity(type), condition);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldForward(LogType actual) => Rank(actual) >= Rank(_min);
|
||||
|
||||
private static int Rank(LogType t) => t switch
|
||||
{
|
||||
LogType.Log => 0,
|
||||
LogType.Warning => 1,
|
||||
LogType.Error => 2,
|
||||
LogType.Assert => 2,
|
||||
LogType.Exception => 3,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static string Severity(LogType t) => t switch
|
||||
{
|
||||
LogType.Warning => "warn",
|
||||
LogType.Error => "error",
|
||||
LogType.Assert => "error",
|
||||
LogType.Exception => "error",
|
||||
_ => "info",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc803059ceea74ae19a6e02ba5292d3e
|
||||
Reference in New Issue
Block a user