Ad fixes and appsflyer

This commit is contained in:
Savya Bikram Shah
2026-06-01 15:01:15 +05:45
parent 140a252350
commit f9826325c6
159 changed files with 9346 additions and 3084 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ddc06781290bc42b480134ac73d6a269
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
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,
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a44c903b7fed4e889c9fc91430e4b71

View File

@@ -103,6 +103,10 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
await ShowRewardedAdAsync(ct);
// Ad SDKs can resume the continuation on a background thread; the scene load below
// must run on the Unity main thread. No-op when already there.
await UniTask.SwitchToMainThread(ct);
var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f));
var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f));
await _progression.SetLastOpenedAsync(templateId);

View File

@@ -76,6 +76,13 @@ namespace Darkmatter.Services.Ads
#if GOOGLE_MOBILE_ADS
ApplyRequestConfiguration();
// AdMob raises ad callbacks (OnAdFullScreenContentClosed, etc.) on a background
// thread by default. UniTask continuations then resume off the main thread, so the
// scene load that runs after a rewarded ad closes calls SceneManager/Addressables
// from a background thread and throws — leaving the loading screen stuck at 0%.
// Force all ad events onto the Unity main thread.
MobileAds.RaiseAdEventsOnUnityMainThread = true;
var tcs = new UniTaskCompletionSource<bool>();
MobileAds.Initialize(_ => tcs.TrySetResult(true));

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c6df39d33cdd84ce1ab71ee4e5026682
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8eef8a073e91748a09d25cf48e413e05
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
using UnityEngine;
namespace Darkmatter.Services.LogRocket.Configuration
{
[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("Session Replay")]
[Tooltip("Periodically capture a screenshot and upload it as a replay frame.")]
public bool EnableSessionReplay = true;
[Tooltip("Seconds between captured frames. Lower interval = larger payload.")]
[Range(0.25f, 10f)] public float CaptureIntervalSeconds = 2f;
[Tooltip("Max width in pixels for captured frames. Frames downscale to fit.")]
[Range(128, 2048)] public int MaxFrameWidth = 720;
[Tooltip("JPEG quality (1-100) used when encoding replay frames.")]
[Range(10, 100)] public int JpegQuality = 60;
[Header("Behaviour")]
[Tooltip("Disable LogRocket entirely in the editor.")]
public bool DisableInEditor = true;
[Tooltip("Auto-start session on service init.")]
public bool AutoStart = true;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e20e49adf823a4918b4b7823d203a28b

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c7656001fcf7748289669ff7f1bba786
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
<?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.
Verify the versions against the current LogRocket release before shipping:
iOS pod: https://cocoapods.org/pods/LogRocket (ObjC class: LROSDK)
Android Maven: confirm group/artifact + the bridge's reflected class name
in LogRocketUnityBridge.java (com.logrocket.core.LogRocket is
an UNVERIFIED guess).
-->
<dependencies>
<iosPods>
<iosPod name="LogRocket" version="~> 1.47" addToAllTargets="false">
<sources>
<source>https://github.com/CocoaPods/Specs</source>
</sources>
</iosPod>
</iosPods>
<androidPackages>
<!-- TODO: confirm the real Maven coordinate. Placeholder commented out so it
does not break Android resolution until verified.
<androidPackage spec="com.logrocket:logrocket:+" />
-->
</androidPackages>
</dependencies>

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e2806d88a0ebc4aaeb06d2fd098c7555
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2d3c637029058429a8ea92a186cba06f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
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>();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f78e90a5f08704f62b3624ff47ebf66c

View File

@@ -0,0 +1,19 @@
{
"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
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5a1957cf2cabd413bad46969b408df0e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 491835b65b47144db979541bb7e37499
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
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 CaptureFrame(byte[] jpegBytes, int width, int height);
void StartReplay();
void StopReplay();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14484983d15d0451ebe505bb8f93f447

View File

@@ -0,0 +1,142 @@
#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";
private AndroidJavaObject _bridge;
public bool IsAvailable => _bridge != null;
public string SessionUrl => SafeCall<string>("getSessionUrl");
public void Init(string appId)
{
try
{
using var bridgeClass = new AndroidJavaClass(BridgeClass);
using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
_bridge = bridgeClass.CallStatic<AndroidJavaObject>("getInstance");
_bridge.Call("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.Call("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.Call("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.Call("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.Call("logException", message ?? string.Empty, stackTrace ?? string.Empty); }
catch (Exception e) { Debug.LogError($"[LogRocket] logException failed: {e}"); }
}
public void CaptureFrame(byte[] jpeg, int width, int height)
{
if (_bridge == null || jpeg == null || jpeg.Length == 0) return;
try { _bridge.Call("captureFrame", jpeg, width, height); }
catch (Exception e) { Debug.LogError($"[LogRocket] captureFrame failed: {e}"); }
}
public void StartReplay()
{
if (_bridge == null) return;
try { _bridge.Call("startReplay"); }
catch (Exception e) { Debug.LogError($"[LogRocket] startReplay failed: {e}"); }
}
public void StopReplay()
{
if (_bridge == null) return;
try { _bridge.Call("stopReplay"); }
catch (Exception e) { Debug.LogError($"[LogRocket] stopReplay failed: {e}"); }
}
private T SafeCall<T>(string method)
{
if (_bridge == null) return default;
try { return _bridge.Call<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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e384b24344c8e4899be0036dd9edc333

View File

@@ -0,0 +1,145 @@
#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_captureFrame(IntPtr bytes, int length, int width, int height);
[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 CaptureFrame(byte[] jpeg, int width, int height)
{
if (!_initialised || jpeg == null || jpeg.Length == 0) return;
var handle = GCHandle.Alloc(jpeg, GCHandleType.Pinned);
try { _lr_captureFrame(handle.AddrOfPinnedObject(), jpeg.Length, width, height); }
catch (Exception e) { Debug.LogError($"[LogRocket] captureFrame failed: {e}"); }
finally { handle.Free(); }
}
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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd81dfea0269d4dec8e97e8756b80801

View File

@@ -0,0 +1,34 @@
#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 CaptureFrame(byte[] jpeg, int w, int h) { }
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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 545348daa325e4c809265a9f76584f19

View File

@@ -0,0 +1,170 @@
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 SessionReplayCapture _replay;
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();
}
if (_config.EnableSessionReplay && _config.AutoStart)
StartSessionReplay();
_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));
}
public void StartSessionReplay()
{
Run(() =>
{
_native.StartReplay();
if (_replay == null)
_replay = new SessionReplayCapture(_native, _config);
_replay.Start();
});
}
public void StopSessionReplay()
{
Run(() =>
{
_replay?.Stop();
_native.StopReplay();
});
}
public void Dispose()
{
_logForwarder?.Disable();
_logForwarder = null;
_replay?.Dispose();
_replay = 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
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d5c8a2c56a83847a3b480e4a13fba63d

View File

@@ -0,0 +1,110 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Services.LogRocket.Configuration;
using UnityEngine;
namespace Darkmatter.Services.LogRocket.Systems
{
internal sealed class SessionReplayCapture : IDisposable
{
private readonly ILogRocketNative _native;
private readonly LogRocketConfig _config;
private CancellationTokenSource _cts;
private bool _running;
public SessionReplayCapture(ILogRocketNative native, LogRocketConfig config)
{
_native = native;
_config = config;
}
public void Start()
{
if (_running) return;
_running = true;
_cts = new CancellationTokenSource();
Loop(_cts.Token).Forget();
}
public void Stop()
{
if (!_running) return;
_running = false;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
public void Dispose() => Stop();
private async UniTaskVoid Loop(CancellationToken ct)
{
float interval = Mathf.Max(0.25f, _config.CaptureIntervalSeconds);
while (!ct.IsCancellationRequested && _running)
{
try
{
// Must be true end-of-frame: ScreenCapture reads the backbuffer after
// rendering completes. LastPostLateUpdate fires before render finish and
// makes CaptureScreenshotAsTexture return an invalid texture.
await UniTask.WaitForEndOfFrame(ct);
if (ct.IsCancellationRequested) break;
CaptureOnce();
}
catch (OperationCanceledException) { break; }
catch (Exception e)
{
Debug.LogError($"[LogRocket] replay capture failed: {e}");
}
try { await UniTask.Delay(TimeSpan.FromSeconds(interval), cancellationToken: ct); }
catch (OperationCanceledException) { break; }
}
}
private void CaptureOnce()
{
var src = ScreenCapture.CaptureScreenshotAsTexture();
if (src == null) return;
Texture2D scaled = null;
try
{
int targetW = Mathf.Min(_config.MaxFrameWidth, src.width);
int targetH = Mathf.RoundToInt(src.height * (targetW / (float)src.width));
scaled = targetW == src.width && targetH == src.height ? src : Resize(src, targetW, targetH);
var jpeg = scaled.EncodeToJPG(Mathf.Clamp(_config.JpegQuality, 10, 100));
_native.CaptureFrame(jpeg, scaled.width, scaled.height);
}
finally
{
if (scaled != null && scaled != src) UnityEngine.Object.Destroy(scaled);
UnityEngine.Object.Destroy(src);
}
}
private static Texture2D Resize(Texture2D src, int w, int h)
{
var rt = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32);
var prev = RenderTexture.active;
try
{
RenderTexture.active = rt;
GL.Clear(false, true, Color.clear);
Graphics.Blit(src, rt);
var dst = new Texture2D(w, h, TextureFormat.RGBA32, false);
dst.ReadPixels(new Rect(0, 0, w, h), 0, 0);
dst.Apply();
return dst;
}
finally
{
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1aae6350f04254f3dabfb7a552a50cb4

View File

@@ -0,0 +1,81 @@
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",
};
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dc803059ceea74ae19a6e02ba5292d3e