From 60995fc2cb703a8a4177cf91da5c5293b33e3ff5 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 3 Jul 2026 12:46:56 +0300 Subject: [PATCH 1/9] Add session replay capturing for iOS --- .../Private/Apple/AppleSentrySubsystem.cpp | 31 +++++++++++++++++++ .../Private/Apple/AppleSentrySubsystem.h | 11 +++++++ .../Sentry/Private/Mac/MacSentrySubsystem.cpp | 28 ----------------- .../Sentry/Private/Mac/MacSentrySubsystem.h | 10 ------ plugin-dev/Source/Sentry/Sentry.Build.cs | 18 +++++++++-- sample/SentryPlayground.uproject | 4 +-- 6 files changed, 60 insertions(+), 42 deletions(-) diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp index 0eda79f75..39be56155 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp @@ -253,10 +253,32 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, co // Wait synchronously until sentry-cocoa initialization finished in main thread dispatch_group_wait(sentryDispatchGroup, DISPATCH_TIME_FOREVER); dispatch_release(sentryDispatchGroup); + + if (IsEnabled()) + { +#ifdef USE_SENTRY_SESSION_REPLAY + if (settings->AttachSessionReplay) + { + SessionReplay = MakeUnique(); + if (!SessionReplay->Initialize(settings, GetReplayPath())) + { + SessionReplay.Reset(); + } + } +#endif + } } void FAppleSentrySubsystem::Close() { +#ifdef USE_SENTRY_SESSION_REPLAY + if (SessionReplay) + { + SessionReplay->Shutdown(); + SessionReplay.Reset(); + } +#endif + [SENTRY_APPLE_CLASS(SentryObjCSDK) flush:0]; [SENTRY_APPLE_CLASS(SentryObjCSDK) close]; } @@ -799,4 +821,13 @@ FString FAppleSentrySubsystem::GetLatestSessionReplay() const return Replays[0]; } +#ifdef USE_SENTRY_SESSION_REPLAY +FString FAppleSentrySubsystem::GetReplayPath() const +{ + const FString ReplayId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens).ToLower(); + const FString ReplayPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryReplays"), FString::Printf(TEXT("replay-%s.mp4"), *ReplayId)); + return FPaths::ConvertRelativePathToFull(ReplayPath); +} +#endif + #endif // !USE_SENTRY_NATIVE diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h index 2dece90b4..bf2e2cddf 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h @@ -6,6 +6,10 @@ #include "Interface/SentrySubsystemInterface.h" +#ifdef USE_SENTRY_SESSION_REPLAY +#include "SessionReplay/SentrySessionReplayRecorder.h" +#endif + class FAppleSentrySubsystem : public ISentrySubsystem { public: @@ -80,6 +84,13 @@ class FAppleSentrySubsystem : public ISentrySubsystem int32 maxAttachmentSize = 0; FString PrevSessionReplayPath; + +private: +#ifdef USE_SENTRY_SESSION_REPLAY + FString GetReplayPath() const; + + TUniquePtr SessionReplay; +#endif }; #endif // !USE_SENTRY_NATIVE diff --git a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp index c5a6138ed..56ca4cb57 100644 --- a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.cpp @@ -116,30 +116,11 @@ void FMacSentrySubsystem::InitWithSettings(const USentrySettings* settings, cons TryCaptureScreenshot(); }); } - -#ifdef USE_SENTRY_SESSION_REPLAY - if (settings->AttachSessionReplay) - { - SessionReplay = MakeUnique(); - if (!SessionReplay->Initialize(settings, GetReplayPath())) - { - SessionReplay.Reset(); - } - } -#endif } } void FMacSentrySubsystem::Close() { -#ifdef USE_SENTRY_SESSION_REPLAY - if (SessionReplay) - { - SessionReplay->Shutdown(); - SessionReplay.Reset(); - } -#endif - if (OnHandleSystemErrorDelegateHandle.IsValid()) { FCoreDelegates::OnHandleSystemError.Remove(OnHandleSystemErrorDelegateHandle); @@ -149,15 +130,6 @@ void FMacSentrySubsystem::Close() FAppleSentrySubsystem::Close(); } -#ifdef USE_SENTRY_SESSION_REPLAY -FString FMacSentrySubsystem::GetReplayPath() const -{ - const FString ReplayId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens).ToLower(); - const FString ReplayPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryReplays"), FString::Printf(TEXT("replay-%s.mp4"), *ReplayId)); - return FPaths::ConvertRelativePathToFull(ReplayPath); -} -#endif - TSharedPtr FMacSentrySubsystem::CaptureEnsure(const FString& type, const FString& message) { TSharedPtr id = FAppleSentrySubsystem::CaptureEnsure(type, message); diff --git a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h index ad52476bf..c49eb73b8 100644 --- a/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Mac/MacSentrySubsystem.h @@ -31,10 +31,6 @@ class FMacSentrySubsystem : public FGenericPlatformSentrySubsystem #include "Apple/AppleSentrySubsystem.h" -#ifdef USE_SENTRY_SESSION_REPLAY -#include "SessionReplay/SentrySessionReplayRecorder.h" -#endif - class FMacSentrySubsystem : public FAppleSentrySubsystem { public: @@ -53,12 +49,6 @@ class FMacSentrySubsystem : public FAppleSentrySubsystem private: FDelegateHandle OnHandleSystemErrorDelegateHandle; - -#ifdef USE_SENTRY_SESSION_REPLAY - FString GetReplayPath() const; - - TUniquePtr SessionReplay; -#endif }; #endif diff --git a/plugin-dev/Source/Sentry/Sentry.Build.cs b/plugin-dev/Source/Sentry/Sentry.Build.cs index 2518078b4..190ee7e6a 100644 --- a/plugin-dev/Source/Sentry/Sentry.Build.cs +++ b/plugin-dev/Source/Sentry/Sentry.Build.cs @@ -251,8 +251,7 @@ public Sentry(ReadOnlyTargetRules Target) : base(Target) } } - if (bAttachSessionReplay && IsPluginEnabled(Target, "AVCodecsCore") && - (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Mac || Target.Platform == UnrealTargetPlatform.Linux)) + if (bAttachSessionReplay && IsSessionReplaySupported(Target) && IsPluginEnabled(Target, "AVCodecsCore")) { PrivateDependencyModuleNames.AddRange(new string[] { @@ -288,6 +287,21 @@ private bool IsPluginEnabled(ReadOnlyTargetRules Target, string PluginName) return false; } + private bool IsSessionReplaySupported(ReadOnlyTargetRules Target) + { + // On Windows/Linux Arm64, session replay capturing is not supported + // On Android, session replay capturing is handled by sentry-java + +#if UE_5_8_OR_LATER + if (Target.Platform == UnrealTargetPlatform.IOS) + { + return true; + } +#endif + + return Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Mac || Target.Platform == UnrealTargetPlatform.Linux; + } + private void StageCrashReporterResources(ReadOnlyTargetRules Target) { if (Target.Type == TargetType.Editor) diff --git a/sample/SentryPlayground.uproject b/sample/SentryPlayground.uproject index 3637218c8..85d12df90 100644 --- a/sample/SentryPlayground.uproject +++ b/sample/SentryPlayground.uproject @@ -38,7 +38,7 @@ { "Name": "AVCodecsCore", "Enabled": true, - "PlatformAllowList": [ "Win64", "Mac", "Linux" ] + "PlatformAllowList": [ "Win64", "Mac", "Linux", "IOS" ] }, { "Name": "NVCodecs", @@ -48,7 +48,7 @@ { "Name": "VTCodecs", "Enabled": true, - "PlatformAllowList": [ "Mac" ] + "PlatformAllowList": [ "Mac", "IOS" ] }, { "Name": "AMFCodecs", From 7bc5170a86a311609fc1c1419d6b21bf8c9caaca Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 3 Jul 2026 14:22:07 +0300 Subject: [PATCH 2/9] Extend Mac platform guards to Apple --- .../SessionReplay/SentryBackBufferCapture.cpp | 14 +++++++------- .../SessionReplay/SentryBackBufferCapture.h | 6 +++--- .../Private/SessionReplay/SentryVideoEncoder.cpp | 10 +++++----- .../Private/SessionReplay/SentryVideoEncoder.h | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.cpp index 376977dd9..4644a7123 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.cpp @@ -117,12 +117,12 @@ void FSentryBackBufferCapture::CaptureBackBuffer_RenderThread(const FTextureRHIR FTextureRHIRef ScratchTex = AcquireCachedTexture_RenderThread(Scratch, W, H, BackBuffer->GetFormat(), SrvRt, ERHIAccess::SRVGraphics, TEXT("SentrySessionReplayScratch")); -#if PLATFORM_MAC +#if PLATFORM_APPLE FTextureRHIRef ConvertedTex = AcquireCachedTexture_RenderThread(Converted, W, H, PF_B8G8R8A8, SrvRt, ERHIAccess::SRVGraphics, TEXT("SentrySessionReplayConverted")); #endif -#if PLATFORM_MAC +#if PLATFORM_APPLE // VT requires pool slot to have CPUReadback flag so it can be read via Metal's getBytes(). // Note: Metal forbids RT|CPUReadback on the same texture so an extra copy is needed (step 3) constexpr ETextureCreateFlags PoolFlags = ETextureCreateFlags::CPUReadback; @@ -149,7 +149,7 @@ void FSentryBackBufferCapture::CaptureBackBuffer_RenderThread(const FTextureRHIR return; } -#if PLATFORM_MAC +#if PLATFORM_APPLE if (!ConvertedTex.IsValid()) { return; @@ -161,7 +161,7 @@ void FSentryBackBufferCapture::CaptureBackBuffer_RenderThread(const FTextureRHIR return; } -#if PLATFORM_MAC +#if PLATFORM_APPLE FTextureRHIRef DrawTarget = ConvertedTex; #else FTextureRHIRef DrawTarget = EncoderTex; @@ -202,8 +202,8 @@ void FSentryBackBufferCapture::CaptureBackBuffer_RenderThread(const FTextureRHIR GraphBuilder.Execute(); } -#if PLATFORM_MAC - // Step 3 (Mac only): hardware-copy Converted -> EncoderPool slot. Both are BGRA8 same-format +#if PLATFORM_APPLE + // Step 3 (Apple only): hardware-copy Converted -> EncoderPool slot. Both are BGRA8 same-format { FRHITransitionInfo Transitions[] = { FRHITransitionInfo(DrawTarget.GetReference(), ERHIAccess::Unknown, ERHIAccess::CopySrc), @@ -217,7 +217,7 @@ void FSentryBackBufferCapture::CaptureBackBuffer_RenderThread(const FTextureRHIR } #endif -#if PLATFORM_MAC +#if PLATFORM_APPLE RHICmdList.Transition(FRHITransitionInfo(EncoderTex.GetReference(), ERHIAccess::CopyDest, ERHIAccess::CPURead)); #else RHICmdList.Transition(FRHITransitionInfo(EncoderTex.GetReference(), ERHIAccess::Unknown, ERHIAccess::SRVGraphics)); diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.h b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.h index 4c620f6c8..07517cf6f 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.h +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryBackBufferCapture.h @@ -28,9 +28,9 @@ class SWindow; * 2. AddDrawTexturePass scratch -> a BGRA8 destination. When the scratch * format is already BGRA8 this stays a hardware copy; otherwise the * engine's built-in pixel-shader path converts HDR/10-bit to BGRA8. - * Destination is the encoder pool slot on Windows; on Mac it's the + * Destination is the encoder pool slot on Windows; on Apple platforms it's the * "converted" texture because Metal forbids RenderTargetable | CPUReadback. - * 3. Mac only: hardware-copy converted -> encoder pool slot (CPUReadback BGRA8). + * 3. Apple only: hardware-copy converted -> encoder pool slot (CPUReadback BGRA8). * The pool slot is then handed to the encoder. */ class FSentryBackBufferCapture @@ -94,7 +94,7 @@ class FSentryBackBufferCapture // don't carry the SRV flag, so they can't be sampled in a shader directly FCachedTexture Scratch; - // BGRA8 RenderTargetable texture. Used on Mac as the draw pass output + // BGRA8 RenderTargetable texture. Used on Apple platforms as the draw pass output // before the final hardware copy into the CPUReadback EncoderPool slot FCachedTexture Converted; diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp index a09e4d8f6..e63cc1c45 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp @@ -175,7 +175,7 @@ uint32 FSentryVideoEncoder::Run() } // VT interprets SendFrame's timestamp as microseconds (see Engine's VideoEncoderVT.hpp) -#if PLATFORM_MAC +#if PLATFORM_APPLE static constexpr double SendTimestampScale = 1'000'000.0; #else static constexpr double SendTimestampScale = 1'000.0; @@ -184,8 +184,8 @@ uint32 FSentryVideoEncoder::Run() const double TimestampSeconds = FMath::Max(0.0, Frame.CaptureTimeSeconds - CaptureTimeBaseSeconds); const double ScaledTimestamp = TimestampSeconds * SendTimestampScale; -#if PLATFORM_MAC - // Restart the encoder every hour of recording. On Mac this stays well clear of the +#if PLATFORM_APPLE + // Restart the encoder every hour of recording. On Apple platforms this stays well clear of the // uint32-microseconds wrap at ~71 min which would otherwise feed VT a backward PTS // and corrupt all subsequent fragments. On Windows the natural wrap is at ~49 days // so realistically periodic refresh of encoder state won't be needed there @@ -270,7 +270,7 @@ bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 Resourc Config.bFillData = 0; Config.MultipassMode = EMultipassMode::Disabled; -#if PLATFORM_MAC +#if PLATFORM_APPLE // Work around a VT bug where H.264 Auto maps to a null EntropyCodingMode // causing a crash in CFStringGetLength. Use CABAC instead (supported by Main/High profiles) Config.EntropyCodingMode = EH264EntropyCodingMode::CABAC; @@ -384,7 +384,7 @@ void FSentryVideoEncoder::DrainPackets() // when the source renders below the configured target rate). A skipped // packet never updates the marker, so its interval folds into the next // sample. The first sample falls back to a nominal 1/Framerate -#if PLATFORM_MAC +#if PLATFORM_APPLE const uint32 PacketTimestampMs = static_cast(FPlatformTime::ToMilliseconds64(Packet.Timestamp)); #else const uint32 PacketTimestampMs = static_cast(Packet.Timestamp); diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h index cb94dabb8..3ac3c3411 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h @@ -66,7 +66,7 @@ class FSentryVideoEncoder : public FRunnable // Tears down the current encoder and resets per-encoder state so the next frame // re-baselines against a fresh VT timestamp origin and republishes a new init // segment. Used to avoid uint32 overflow of the SendFrame timestamp (~71 min of - // microseconds on Mac, ~49 days of milliseconds on Windows). Must be called only + // microseconds on Apple platforms, ~49 days of milliseconds on Windows). Must be called only // from the encoder thread void Restart(); From 5ccd06db68b864f3a89f0861892e1c5c9492dc97 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 3 Jul 2026 14:34:52 +0300 Subject: [PATCH 3/9] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63787fe41..86386d184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add experimental session replay capturing on iOS (UE 5.8+) ([#1462](https://github.com/getsentry/sentry-unreal/pull/1462)) + ### Dependencies - Bump Android Gradle Plugin from v6.12.0 to v6.13.0 ([#1452](https://github.com/getsentry/sentry-unreal/pull/1452)) From 182c914c07cbc4dc81fbcf6f4a327b3a18b9d6db Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 6 Jul 2026 09:03:17 +0300 Subject: [PATCH 4/9] Add device screen orientation check --- .../SessionReplay/SentryVideoEncoder.cpp | 32 ++++++++++++++++++- .../SessionReplay/SentryVideoEncoder.h | 8 +++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp index e63cc1c45..7cdf21df4 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp @@ -8,6 +8,7 @@ #include "SentrySessionReplayRecorder.h" #include "HAL/Event.h" +#include "HAL/PlatformMisc.h" #include "HAL/RunnableThread.h" #include "Misc/ScopeLock.h" #include "RHI.h" @@ -27,6 +28,9 @@ FSentryVideoEncoder::FSentryVideoEncoder(FSentrySessionReplayRecorder& InRecorde , BitrateBps(InBitrateKbps * 1000) , FragmentSeconds(InFragmentSeconds) { +#if PLATFORM_IOS + ExpectedOrientation = FPlatformMisc::GetDeviceOrientation(); +#endif } FSentryVideoEncoder::~FSentryVideoEncoder() @@ -237,11 +241,28 @@ uint32 FSentryVideoEncoder::Run() return 0; } +bool FSentryVideoEncoder::ShouldSwapDimensions(uint32 ResourceWidth, uint32 ResourceHeight) const +{ + switch (ExpectedOrientation) + { + case EDeviceScreenOrientation::Portrait: + case EDeviceScreenOrientation::PortraitUpsideDown: + return ResourceWidth > ResourceHeight; + case EDeviceScreenOrientation::LandscapeLeft: + case EDeviceScreenOrientation::LandscapeRight: + return ResourceHeight > ResourceWidth; + default: + return false; + } +} + bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 ResourceHeight) { if (bEncoderOpen) { - if ((ResourceWidth != Width || ResourceHeight != Height) && !bResolutionChanged) + const bool bSameSize = ResourceWidth == Width && ResourceHeight == Height; + const bool bSameTransposedSize = ResourceWidth == Height && ResourceHeight == Width; + if (!bSameSize && !bSameTransposedSize && !bResolutionChanged) { UE_LOG(LogSentrySdk, Warning, TEXT("Session replay: capture resolution changed from %ux%u to %ux%u; recording stays locked to the original size and may be cropped or black."), Width, Height, ResourceWidth, ResourceHeight); @@ -255,6 +276,15 @@ bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 Resourc return false; } + // If the first frame arrives in the device's native (transposed) dimensions + // swap them so the video is written with the correct resolution from the start + if (!ShouldSwapDimensions(ResourceWidth, ResourceHeight)) + { + UE_LOG(LogSentrySdk, Log, TEXT("Session replay: first frame is %ux%u but the app runs in %s orientation; opening the encoder with swapped dimensions."), + ResourceWidth, ResourceHeight, ResourceHeight > ResourceWidth ? TEXT("landscape") : TEXT("portrait")); + Swap(ResourceWidth, ResourceHeight); + } + FVideoEncoderConfigH264 Config; Config.Width = ResourceWidth; Config.Height = ResourceHeight; diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h index 3ac3c3411..3c7c465c6 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.h @@ -58,6 +58,9 @@ class FSentryVideoEncoder : public FRunnable static constexpr int32 MaxQueueDepth = 5; private: + // Checks if frame dimensions match with the app's fixed screen orientation + bool ShouldSwapDimensions(uint32 ResourceWidth, uint32 ResourceHeight) const; + bool EnsureEncoderOpen(uint32 ResourceWidth, uint32 ResourceHeight); // Pulls available packets from the encoder, converts them to AVCC samples and emits a fragment at each keyframe boundary @@ -74,6 +77,11 @@ class FSentryVideoEncoder : public FRunnable bool bEncoderOpen = false; bool bResolutionChanged = false; + + // Screen orientation the app runs in, captured once at construction (iOS only) + // and assumed constant for the session. Unknown disables orientation handling + EDeviceScreenOrientation ExpectedOrientation = EDeviceScreenOrientation::Unknown; + TSharedPtr> Encoder; bool bFirstFrameValidated = false; From 490987f73f324baf8bbf0eb2c8d11ac902aa72f9 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Mon, 6 Jul 2026 09:13:30 +0300 Subject: [PATCH 5/9] Fix check --- .../Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp index 7cdf21df4..4cc1891dd 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp @@ -278,7 +278,7 @@ bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 Resourc // If the first frame arrives in the device's native (transposed) dimensions // swap them so the video is written with the correct resolution from the start - if (!ShouldSwapDimensions(ResourceWidth, ResourceHeight)) + if (ShouldSwapDimensions(ResourceWidth, ResourceHeight)) { UE_LOG(LogSentrySdk, Log, TEXT("Session replay: first frame is %ux%u but the app runs in %s orientation; opening the encoder with swapped dimensions."), ResourceWidth, ResourceHeight, ResourceHeight > ResourceWidth ? TEXT("landscape") : TEXT("portrait")); From d5cd9ddca7ab57d40e58c2dc005ebcc86e8f5ae3 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 7 Jul 2026 10:23:39 +0300 Subject: [PATCH 6/9] Fix res check --- .../Sentry/Private/SessionReplay/SentryVideoEncoder.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp index 4cc1891dd..6052ff550 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp @@ -261,7 +261,12 @@ bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 Resourc if (bEncoderOpen) { const bool bSameSize = ResourceWidth == Width && ResourceHeight == Height; - const bool bSameTransposedSize = ResourceWidth == Height && ResourceHeight == Width; + + // Transposed frames are expected only when orientation tracking is active (iOS); + // elsewhere a transposed size is a genuine resolution change + const bool bSameTransposedSize = ExpectedOrientation != EDeviceScreenOrientation::Unknown && + ResourceWidth == Height && ResourceHeight == Width; + if (!bSameSize && !bSameTransposedSize && !bResolutionChanged) { UE_LOG(LogSentrySdk, Warning, TEXT("Session replay: capture resolution changed from %ux%u to %ux%u; recording stays locked to the original size and may be cropped or black."), From 27ae589064387d8249b3dc7cd0ac4d3e90c9829f Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 7 Jul 2026 07:23:59 +0000 Subject: [PATCH 7/9] Format code --- .../Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp index 6052ff550..f14902881 100644 --- a/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp +++ b/plugin-dev/Source/Sentry/Private/SessionReplay/SentryVideoEncoder.cpp @@ -265,7 +265,7 @@ bool FSentryVideoEncoder::EnsureEncoderOpen(uint32 ResourceWidth, uint32 Resourc // Transposed frames are expected only when orientation tracking is active (iOS); // elsewhere a transposed size is a genuine resolution change const bool bSameTransposedSize = ExpectedOrientation != EDeviceScreenOrientation::Unknown && - ResourceWidth == Height && ResourceHeight == Width; + ResourceWidth == Height && ResourceHeight == Width; if (!bSameSize && !bSameTransposedSize && !bResolutionChanged) { From b9c8d60587cc8b949cc82108c8a73b25b1b24f4a Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 7 Jul 2026 11:57:10 +0300 Subject: [PATCH 8/9] Fix build errors after merge --- .../Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp | 9 ++++++++- .../Source/Sentry/Private/Apple/AppleSentrySubsystem.h | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp index 39be56155..a4130c6e5 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp @@ -259,10 +259,17 @@ void FAppleSentrySubsystem::InitWithSettings(const USentrySettings* settings, co #ifdef USE_SENTRY_SESSION_REPLAY if (settings->AttachSessionReplay) { + SessionReplayId = FGuid::NewGuid().ToString(EGuidFormats::Digits).ToLower(); + SessionReplay = MakeUnique(); - if (!SessionReplay->Initialize(settings, GetReplayPath())) + if (SessionReplay->Initialize(settings, SessionReplayId, GetReplayPath())) + { + SetContext(TEXT("replay"), { { TEXT("replay_id"), FSentryVariant(SessionReplayId) } }); + } + else { SessionReplay.Reset(); + SessionReplayId.Reset(); } } #endif diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h index bf2e2cddf..a339d69fc 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h @@ -89,6 +89,8 @@ class FAppleSentrySubsystem : public ISentrySubsystem #ifdef USE_SENTRY_SESSION_REPLAY FString GetReplayPath() const; + FString SessionReplayId; + TUniquePtr SessionReplay; #endif }; From 0a70f42db2435c252de6019e4ed54302305f5ddc Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 7 Jul 2026 12:09:34 +0300 Subject: [PATCH 9/9] Fix guid path --- .../Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp index a4130c6e5..951c2742d 100644 --- a/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp @@ -831,8 +831,7 @@ FString FAppleSentrySubsystem::GetLatestSessionReplay() const #ifdef USE_SENTRY_SESSION_REPLAY FString FAppleSentrySubsystem::GetReplayPath() const { - const FString ReplayId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens).ToLower(); - const FString ReplayPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryReplays"), FString::Printf(TEXT("replay-%s.mp4"), *ReplayId)); + const FString ReplayPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("SentryReplays"), FString::Printf(TEXT("replay-%s.mp4"), *SessionReplayId)); return FPaths::ConvertRelativePathToFull(ReplayPath); } #endif