diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 744ed25504..2f187a2865 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,4 +10,4 @@ A brief description of implementation details of this PR. - [ ] Feature or bugfix MUST have appropriate tests (unit, integration) - [ ] Make sure each commit and the PR mention the Issue number or JIRA reference - [ ] Add CHANGELOG entry for user facing changes -- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) (internal)) and run `make api-surface` +- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) [internal]) and run `make api-surface`) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f327debeb..8704a7bc0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # Unreleased +# 2.20.0 / 14-11-2024 + +- [FIX] Fix race condition during consent change, preventing loss of events recorded on the current thread. See [#2063][] +- [IMPROVEMENT] Support mutation of events' attributes. See [#2099][] +- [IMPROVEMENT] Add 'os' and 'device' info to Span events. See [#2104][] +- [FIX] Fix bug in SR that was enforcing full snapshot more often than needed. See [#2092][] + # 2.19.0 / 28-10-2024 - [FEATURE] Add Privacy Overrides in Session Replay. See [#2088][] - [IMPROVEMENT] Add ObjC API for the internal logging/telemetry. See [#2073][] +- [IMPROVEMENT] Support `clipsToBounds` in Session Replay. See [#2083][] # 2.18.0 / 25-09-2024 - [IMPROVEMENT] Add overwrite required (breaking) param to addViewLoadingTime & usage telemetry. See [#2040][] @@ -781,6 +789,11 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#2050]: https://github.com/DataDog/dd-sdk-ios/pull/2050 [#2073]: https://github.com/DataDog/dd-sdk-ios/pull/2073 [#2088]: https://github.com/DataDog/dd-sdk-ios/pull/2088 +[#2083]: https://github.com/DataDog/dd-sdk-ios/pull/2083 +[#2104]: https://github.com/DataDog/dd-sdk-ios/pull/2104 +[#2099]: https://github.com/DataDog/dd-sdk-ios/pull/2099 +[#2063]: https://github.com/DataDog/dd-sdk-ios/pull/2063 +[#2092]: https://github.com/DataDog/dd-sdk-ios/pull/2092 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 647122b78a..c7cd173b5b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -158,7 +158,7 @@ 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */; }; 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */; }; 61054E6C2A6EE10A00AAA894 /* SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E172A6EE10A00AAA894 /* SystemColors.swift */; }; - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */; }; + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */; }; 61054E6E2A6EE10A00AAA894 /* RecordingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */; }; 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */; }; 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */; }; @@ -184,7 +184,6 @@ 61054E842A6EE10A00AAA894 /* UITabBarRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */; }; 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */; }; 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */; }; - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */; }; 61054E882A6EE10A00AAA894 /* ViewTreeRecordingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */; }; 61054E892A6EE10A00AAA894 /* NodeIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */; }; 61054E8A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */; }; @@ -226,7 +225,7 @@ 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */; }; 61054FA82A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */; }; 61054FAA2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */; }; - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */; }; + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */; }; 61054FAD2A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */; }; 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */; }; 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */; }; @@ -307,6 +306,8 @@ 61133C702423993200786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 61193AAE2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */; }; @@ -1004,7 +1005,6 @@ D23F8EA529DDCD38001CFAE8 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FDF29DDC75A005C54A4 /* UIKitMocks.swift */; }; D23F8EA629DDCD38001CFAE8 /* RUMDeviceInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */; }; D23F8EA829DDCD38001CFAE8 /* RUMResourceScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */; }; - D23F8EA929DDCD38001CFAE8 /* RUMOperatingSystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */; }; D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E820425A879AF0084B751 /* RUMDataModelMocks.swift */; }; D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715FB24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift */; }; D23F8EAD29DDCD38001CFAE8 /* RUMEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */; }; @@ -1153,6 +1153,7 @@ D270CDDE2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */; }; D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */; }; D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CBD992BB5DBBB00C766AA /* Mocks.swift */; }; @@ -1263,7 +1264,6 @@ D29A9FAB29DDB483005C54A4 /* RUMUserActionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */; }; D29A9FAC29DDB483005C54A4 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; }; D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */; }; - D29A9FB029DDB483005C54A4 /* RUMOperatingSystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */; }; D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */; }; D29A9FB729DDB483005C54A4 /* ViewIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */; }; D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */; }; @@ -2207,7 +2207,7 @@ 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplay.swift"; sourceTree = ""; }; 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CFType+Safety.swift"; sourceTree = ""; }; 61054E172A6EE10A00AAA894 /* SystemColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemColors.swift; sourceTree = ""; }; - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrame.swift"; sourceTree = ""; }; + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplay.swift"; sourceTree = ""; }; 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinator.swift; sourceTree = ""; }; 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzler.swift; sourceTree = ""; }; 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchSnapshotProducer.swift; sourceTree = ""; }; @@ -2233,7 +2233,6 @@ 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITabBarRecorder.swift; sourceTree = ""; }; 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISegmentRecorder.swift; sourceTree = ""; }; 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsupportedViewRecorder.swift; sourceTree = ""; }; - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ViewAttributes+Copy.swift"; sourceTree = ""; }; 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContext.swift; sourceTree = ""; }; 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeIDGenerator.swift; sourceTree = ""; }; 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowViewTreeSnapshotProducer.swift; sourceTree = ""; }; @@ -2275,7 +2274,7 @@ 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesFlattenerTests.swift; sourceTree = ""; }; 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinatorTests.swift; sourceTree = ""; }; 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplayTests.swift"; sourceTree = ""; }; - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrameTests.swift"; sourceTree = ""; }; + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplayTests.swift"; sourceTree = ""; }; 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowTouchSnapshotProducerTests.swift; sourceTree = ""; }; 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchIdentifierGeneratorTests.swift; sourceTree = ""; }; 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContextTests.swift; sourceTree = ""; }; @@ -2374,6 +2373,7 @@ 611529A425E3DD51004F740E /* ValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisher.swift; sourceTree = ""; }; 611529AD25E3E429004F740E /* ValuePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisherTests.swift; sourceTree = ""; }; 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionDelegate+objc.swift"; sourceTree = ""; }; + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifierTests.swift; sourceTree = ""; }; 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandler.swift; sourceTree = ""; }; 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; @@ -2506,7 +2506,6 @@ 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceSamplingStrategy+objc.swift"; sourceTree = ""; }; 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMMonitorTests.swift; sourceTree = ""; }; 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfo.swift; sourceTree = ""; }; - 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfoTests.swift; sourceTree = ""; }; 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMCommandSubscriber.swift; sourceTree = ""; }; 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMInstrumentation.swift; sourceTree = ""; }; 616F1FAF283E227100651A3A /* LogsFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsFeature.swift; sourceTree = ""; }; @@ -2975,6 +2974,7 @@ D26C49BE288982DA00802B2D /* FeatureUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureUpload.swift; sourceTree = ""; }; D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+SessionReplay.swift"; sourceTree = ""; }; D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; D27CBD992BB5DBBB00C766AA /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionTests.swift; sourceTree = ""; }; @@ -3649,7 +3649,8 @@ 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, 61054E172A6EE10A00AAA894 /* SystemColors.swift */, - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */, + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */, + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */, ); name = Utilities; path = Recorder/Utilities; @@ -3691,7 +3692,6 @@ 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */, 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */, 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */, - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */, 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */, 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */, 61054E272A6EE10A00AAA894 /* NodeRecorders */, @@ -3931,7 +3931,7 @@ isa = PBXGroup; children = ( 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */, - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */, + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */, ); path = Utilties; sourceTree = ""; @@ -5646,7 +5646,6 @@ 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */, 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */, 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */, - 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */, ); path = RUMEvent; sourceTree = ""; @@ -6115,6 +6114,7 @@ D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */, D24C9C5129A7BD12002057CF /* SamplerMock.swift */, D24C9C5429A7C5F3002057CF /* DateProvider.swift */, + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */, D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */, D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */, 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, @@ -8395,7 +8395,6 @@ 962C41A72CA431370050B747 /* SessionReplayPrivacyOverrides.swift in Sources */, 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */, 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */, 96F25A832CC7EA4400459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift in Sources */, 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */, @@ -8412,7 +8411,8 @@ 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */, 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */, - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */, + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */, + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */, 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */, A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */, 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */, @@ -8478,7 +8478,7 @@ 962C41A92CB00FD60050B747 /* DDSessionReplayTests.swift in Sources */, 61054FBD2A6EE1BA00AAA894 /* UIViewRecorderTests.swift in Sources */, 61054F952A6EE1BA00AAA894 /* SessionReplayConfigurationTests.swift in Sources */, - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */, + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */, 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */, 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */, A74A72872B10CE4100771FEB /* ResourceMocks.swift in Sources */, @@ -8911,7 +8911,6 @@ D23F8EA529DDCD38001CFAE8 /* UIKitMocks.swift in Sources */, D23F8EA629DDCD38001CFAE8 /* RUMDeviceInfoTests.swift in Sources */, D23F8EA829DDCD38001CFAE8 /* RUMResourceScopeTests.swift in Sources */, - D23F8EA929DDCD38001CFAE8 /* RUMOperatingSystemInfoTests.swift in Sources */, 3CFF4FA52C0E0FE9006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */, D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */, @@ -8976,6 +8975,7 @@ D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */, D2579553298ABB04008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8E2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6929A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7142B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D2579558298ABB04008A1BE5 /* Encoding.swift in Sources */, @@ -9024,6 +9024,7 @@ D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */, D257957A298ABB83008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8F2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6A29A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7152B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D257957B298ABB83008A1BE5 /* Encoding.swift in Sources */, @@ -9248,7 +9249,6 @@ D29A9FE029DDC75A005C54A4 /* UIKitMocks.swift in Sources */, D29A9FA329DDB483005C54A4 /* RUMDeviceInfoTests.swift in Sources */, D29A9FBC29DDB483005C54A4 /* RUMResourceScopeTests.swift in Sources */, - D29A9FB029DDB483005C54A4 /* RUMOperatingSystemInfoTests.swift in Sources */, 3CFF4FA42C0E0FE8006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D29A9FC629DDBA8A005C54A4 /* RUMDataModelMocks.swift in Sources */, D29A9FD529DDC624005C54A4 /* RUMDataModelsMappingTests.swift in Sources */, diff --git a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift index 49dca3dcfd..649534d9c3 100644 --- a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift @@ -107,9 +107,7 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { applicationID: .mockAny(), urlSessionTracking: .init( resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count + providerDataCount = data?.count ?? 0 providerExpectation.fulfill() return [:] }) @@ -149,17 +147,14 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { ) let providerExpectation = expectation(description: "provider called") - var providerDataCount = 0 - var providerData: Data? + var providerInfo: (resp: URLResponse?, data: Data?, err: Error?)? + RUM.enable( with: .init( applicationID: .mockAny(), urlSessionTracking: .init( - resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count - data.map { providerData = $0 } + resourceAttributesProvider: { _, resp, data, err in + providerInfo = (resp, data, err) providerExpectation.fulfill() return [:] }) @@ -182,20 +177,18 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) let taskExpectation = self.expectation(description: "task completed") - var taskDataCount = 0 - var taskData: Data? - let task = session.dataTask(with: request) { data, _, _ in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - taskDataCount = data!.count - data.map { taskData = $0 } + var taskInfo: (resp: URLResponse?, data: Data?, err: Error?)? + + let task = session.dataTask(with: request) { resp, data, err in + taskInfo = (data, resp, err) taskExpectation.fulfill() } task.resume() wait(for: [providerExpectation, taskExpectation], timeout: 10) - XCTAssertEqual(providerDataCount, taskDataCount) - XCTAssertEqual(providerData, taskData) + XCTAssertEqual(providerInfo?.resp, taskInfo?.resp) + XCTAssertEqual(providerInfo?.data, taskInfo?.data) + XCTAssertEqual(providerInfo?.err as? NSError, taskInfo?.err as? NSError) } class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate { diff --git a/DatadogAlamofireExtension.podspec b/DatadogAlamofireExtension.podspec index 497555694d..8d2508f175 100644 --- a/DatadogAlamofireExtension.podspec +++ b/DatadogAlamofireExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogAlamofireExtension" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." s.description = <<-DESC The DatadogAlamofireExtension pod is deprecated and will no longer be maintained. diff --git a/DatadogCore.podspec b/DatadogCore.podspec index e4bf9503ed..bec669406a 100644 --- a/DatadogCore.podspec +++ b/DatadogCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCore" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift index e6b1096797..34f5530e6b 100644 --- a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -14,14 +14,6 @@ import WatchKit internal final class ApplicationStatePublisher: ContextValuePublisher { typealias Snapshot = AppStateHistory.Snapshot - private static var currentApplicationState: ApplicationState { - #if canImport(WatchKit) - WKExtension.dd.shared.applicationState - #else - UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state - #endif - } - /// The default publisher queue. private static let defaultQueue = DispatchQueue( label: "com.datadoghq.app-state-publisher", @@ -58,19 +50,21 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// Creates a Application state publisher for publishing application state /// history. /// + /// **Note**: It must be called on the main thread. + /// /// - Parameters: - /// - initialState: The initial application state. - /// - queue: The queue for publishing the history. - /// - dateProvider: The date provider for the Application state snapshot timestamp. + /// - appStateProvider: The provider to access the current application state. /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. + /// - dateProvider: The date provider for the Application state snapshot timestamp. + /// - queue: The queue for publishing the history. init( - initialState: AppState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default + appStateProvider: AppStateProvider, + notificationCenter: NotificationCenter, + dateProvider: DateProvider, + queue: DispatchQueue = ApplicationStatePublisher.defaultQueue ) { let initialValue = AppStateHistory( - initialState: initialState, + initialState: appStateProvider.current, date: dateProvider.now ) @@ -81,30 +75,6 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { self.notificationCenter = notificationCenter } - /// Creates a Application state publisher for publishing application state - /// history. - /// - /// **Note**: It must be called on the main thread. - /// - /// - Parameters: - /// - applicationState: The current shared `UIApplication` state. - /// - queue: The queue for publishing the history. - /// - dateProvider: The date provider for the Application state snapshot timestamp. - /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. - convenience init( - applicationState: ApplicationState = ApplicationStatePublisher.currentApplicationState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default - ) { - self.init( - initialState: AppState(applicationState), - queue: queue, - dateProvider: dateProvider, - notificationCenter: notificationCenter - ) - } - func publish(to receiver: @escaping ContextValueReceiver) { queue.async { self.receiver = receiver } notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive), name: ApplicationNotifications.didBecomeActive, object: nil) diff --git a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift index db33ee23b5..1b1c529ff3 100644 --- a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift +++ b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift @@ -24,11 +24,11 @@ internal final class BatteryStatusPublisher: ContextValuePublisher { /// Creates a battery status publisher from the given device. /// /// - Parameters: - /// - device: The `UIDevice` instance. `.current` by default. /// - notificationCenter: The notification center for observing the `UIDevice` battery changes, + /// - device: The `UIDevice` instance. init( - device: UIDevice = .current, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + device: UIDevice ) { self.device = device self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift index 11168138a3..7e314439fe 100644 --- a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift +++ b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift @@ -19,11 +19,11 @@ internal final class LowPowerModePublisher: ContextValuePublisher { /// Creates a low power mode publisher. /// /// - Parameters: - /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. /// - notificationCenter: The notification center for observing the `NSProcessInfoPowerStateDidChange`, + /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. init( - processInfo: ProcessInfo = .processInfo, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + processInfo: ProcessInfo ) { self.initialValue = processInfo.isLowPowerModeEnabled self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index e04d7e2450..189f7280b4 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -160,7 +160,12 @@ internal final class DatadogCore { /// - Parameter trackingConsent: new consent value, which will be applied for all data collected from now on func set(trackingConsent: TrackingConsent) { if trackingConsent != consentPublisher.consent { - allStorages.forEach { $0.migrateUnauthorizedData(toConsent: trackingConsent) } + contextProvider.queue.async { [allStorages] in + // RUM-3175: To prevent race conditions with ongoing "event write" operations, + // data migration must be synchronized on the context queue. This guarantees that + // all latest events have been written before migration occurs. + allStorages.forEach { $0.migrateUnauthorizedData(toConsent: trackingConsent) } + } consentPublisher.consent = trackingConsent } } @@ -400,8 +405,11 @@ extension DatadogContextProvider { applicationVersion: String, sdkInitDate: Date, device: DeviceInfo, + processInfo: ProcessInfo, dateProvider: DateProvider, - serverDateProvider: ServerDateProvider + serverDateProvider: ServerDateProvider, + notificationCenter: NotificationCenter, + appStateProvider: AppStateProvider ) { let context = DatadogContext( site: site, @@ -442,14 +450,18 @@ extension DatadogContextProvider { #endif #if os(iOS) && !targetEnvironment(simulator) - subscribe(\.batteryStatus, to: BatteryStatusPublisher()) - subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher()) + subscribe(\.batteryStatus, to: BatteryStatusPublisher(notificationCenter: notificationCenter, device: .current)) + subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher(notificationCenter: notificationCenter, processInfo: processInfo)) #endif #if os(iOS) || os(tvOS) DispatchQueue.main.async { // must be call on the main thread to read `UIApplication.State` - let applicationStatePublisher = ApplicationStatePublisher(dateProvider: dateProvider) + let applicationStatePublisher = ApplicationStatePublisher( + appStateProvider: appStateProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider + ) self.subscribe(\.applicationStateHistory, to: applicationStatePublisher) } #endif diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 5e3a487ce1..a731ff8a09 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -226,6 +226,12 @@ public enum Datadog { internal var httpClientFactory: ([AnyHashable: Any]?) -> HTTPClient = { proxyConfiguration in URLSessionClient(proxyConfiguration: proxyConfiguration) } + + /// The default notification center used for subscribing to app lifecycle events and system notifications. + internal var notificationCenter: NotificationCenter = .default + + /// The default application state provider for accessing [application state](https://developer.apple.com/documentation/uikit/uiapplication/state). + internal var appStateProvider: AppStateProvider = DefaultAppStateProvider() } /// Verbosity level of Datadog SDK. Can be used for debugging purposes. @@ -403,6 +409,89 @@ public enum Datadog { registerObjcExceptionHandlerOnce() + try isValid(clientToken: configuration.clientToken) + try isValid(env: configuration.env) + + let core = try DatadogCore( + configuration: configuration, + trackingConsent: trackingConsent, + instanceName: instanceName + ) + + CITestIntegration.active?.startIntegration() + + CoreRegistry.register(core, named: instanceName) + deleteV1Folders(in: core) + + DD.logger = InternalLogger( + dateProvider: configuration.dateProvider, + timeZone: .current, + printFunction: consolePrint, + verbosityLevel: { Datadog.verbosityLevel } + ) + + return core + } + + private static func deleteV1Folders(in core: DatadogCore) { + let deprecated = ["com.datadoghq.logs", "com.datadoghq.traces", "com.datadoghq.rum"].compactMap { + try? Directory.cache().subdirectory(path: $0) // ignore errors - deprecated paths likely do not exist + } + + core.readWriteQueue.async { + // ignore errors + deprecated.forEach { try? FileManager.default.removeItem(at: $0.url) } + } + } + + /// Flushes all authorised data for each feature, tears down and deinitializes the SDK. + /// - It flushes all data authorised for each feature by performing its arbitrary upload (without retrying). + /// - It completes all pending asynchronous work in each feature. + /// + /// This is highly experimental API and only supported in tests. +#if DD_SDK_COMPILED_FOR_TESTING + public static func flushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { + internalFlushAndDeinitialize(instanceName: instanceName) + } +#endif + + internal static func internalFlushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { + // Unregister core instance: + let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore + // Flush and tear down SDK core: + core?.flushAndTearDown() + } +} + +private func isValid(env: String) throws { + /// 1. cannot be more than 200 chars (including `env:` prefix) + /// 2. cannot end with `:` + /// 3. can contain letters, numbers and _:./-_ (other chars are converted to _ at backend) + let regex = #"^[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]$"# + if env.range(of: regex, options: .regularExpression, range: nil, locale: nil) == nil { + throw ProgrammerError(description: "`env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)") + } +} + +private func isValid(clientToken: String) throws { + if clientToken.isEmpty { + throw ProgrammerError(description: "`clientToken` cannot be empty.") + } +} + +extension DatadogCore { + /// The primary entry point for creating a `DatadogCore` instance. + /// + /// - Parameters: + /// - configuration: A configuration object that encapsulates both user-defined options and internal dependencies + /// passed to SDK's downstream components. + /// - trackingConsent: The user's consent regarding data tracking for the SDK. + /// - instanceName: A unique name for this SDK instance. + convenience init( + configuration: Datadog.Configuration, + trackingConsent: TrackingConsent, + instanceName: String + ) throws { let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) if debug { consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) @@ -434,8 +523,7 @@ public enum Datadog { ) let isRunFromExtension = bundleType == .iOSAppExtension - // Set default `DatadogCore`: - let core = DatadogCore( + self.init( directory: try CoreDirectory( in: configuration.systemDirectory(), instanceName: instanceName, @@ -448,9 +536,9 @@ public enum Datadog { encryption: configuration.encryption, contextProvider: DatadogContextProvider( site: configuration.site, - clientToken: try ifValid(clientToken: configuration.clientToken), + clientToken: configuration.clientToken, service: service, - env: try ifValid(env: configuration.env), + env: configuration.env, version: applicationVersion, buildNumber: applicationBuildNumber, buildId: buildId, @@ -464,9 +552,12 @@ public enum Datadog { applicationBundleType: bundleType, applicationVersion: applicationVersion, sdkInitDate: configuration.dateProvider.now, - device: DeviceInfo(), + device: DeviceInfo(processInfo: configuration.processInfo), + processInfo: configuration.processInfo, dateProvider: configuration.dateProvider, - serverDateProvider: configuration.serverDateProvider + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter, + appStateProvider: configuration.appStateProvider ), applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, @@ -474,7 +565,7 @@ public enum Datadog { isRunFromExtension: isRunFromExtension ) - core.telemetry.configuration( + telemetry.configuration( backgroundTasksEnabled: configuration.backgroundTasksEnabled, batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), batchSize: performance.uploaderWindow.toInt64Milliseconds, @@ -482,66 +573,5 @@ public enum Datadog { useLocalEncryption: configuration.encryption != nil, useProxy: configuration.proxyConfiguration != nil ) - - CITestIntegration.active?.startIntegration() - - CoreRegistry.register(core, named: instanceName) - deleteV1Folders(in: core) - - DD.logger = InternalLogger( - dateProvider: configuration.dateProvider, - timeZone: .current, - printFunction: consolePrint, - verbosityLevel: { Datadog.verbosityLevel } - ) - - return core - } - - private static func deleteV1Folders(in core: DatadogCore) { - let deprecated = ["com.datadoghq.logs", "com.datadoghq.traces", "com.datadoghq.rum"].compactMap { - try? Directory.cache().subdirectory(path: $0) // ignore errors - deprecated paths likely do not exist - } - - core.readWriteQueue.async { - // ignore errors - deprecated.forEach { try? FileManager.default.removeItem(at: $0.url) } - } - } - - /// Flushes all authorised data for each feature, tears down and deinitializes the SDK. - /// - It flushes all data authorised for each feature by performing its arbitrary upload (without retrying). - /// - It completes all pending asynchronous work in each feature. - /// - /// This is highly experimental API and only supported in tests. -#if DD_SDK_COMPILED_FOR_TESTING - public static func flushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { - internalFlushAndDeinitialize(instanceName: instanceName) - } -#endif - - internal static func internalFlushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { - // Unregister core instance: - let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore - // Flush and tear down SDK core: - core?.flushAndTearDown() - } -} - -private func ifValid(env: String) throws -> String { - /// 1. cannot be more than 200 chars (including `env:` prefix) - /// 2. cannot end with `:` - /// 3. can contain letters, numbers and _:./-_ (other chars are converted to _ at backend) - let regex = #"^[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]$"# - if env.range(of: regex, options: .regularExpression, range: nil, locale: nil) == nil { - throw ProgrammerError(description: "`env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)") - } - return env -} - -private func ifValid(clientToken: String) throws -> String { - if clientToken.isEmpty { - throw ProgrammerError(description: "`clientToken` cannot be empty.") } - return clientToken } diff --git a/DatadogCore/Sources/Versioning.swift b/DatadogCore/Sources/Versioning.swift index ab060cdf20..a11617c93b 100644 --- a/DatadogCore/Sources/Versioning.swift +++ b/DatadogCore/Sources/Versioning.swift @@ -1,3 +1,3 @@ // GENERATED FILE: Do not edit directly -internal let __sdkVersion = "2.19.0" +internal let __sdkVersion = "2.20.0" diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift index 26c463970a..ffa82c98cf 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift @@ -26,9 +26,9 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), - dateProvider: SystemDateProvider(), - notificationCenter: notificationCenter + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: SystemDateProvider() ) // When @@ -57,9 +57,9 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), - dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0), - notificationCenter: notificationCenter + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0) ) var receivedHistoryStates: [AppState?] = [] diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift index fdd07afa3f..21465677f1 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift @@ -19,7 +19,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryState: .unknown) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.state, .charging) @@ -38,7 +38,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryLevel: 0.5) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.level, 0.75) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift index 1eb2389e7e..7e79241b5c 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift @@ -17,8 +17,8 @@ class LowPowerModePublisherTests: XCTestCase { // Given let isLowPowerModeEnabled: Bool = .random() let publisher = LowPowerModePublisher( - processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled), - notificationCenter: notificationCenter + notificationCenter: notificationCenter, + processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled) ) XCTAssertEqual(publisher.initialValue, isLowPowerModeEnabled) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift index 9a51a52d7f..acf94fe544 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -78,6 +78,65 @@ class DatadogCoreTests: XCTestCase { XCTAssertEqual(requestBuilderSpy.requestParameters.count, 1, "It should send only one request") } + func testWhenWritingEventsWithPendingConsentThenGranted_itUploadsAllEvents() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .combining( + storagePerformance: StoragePerformanceMock.readAllFiles, + uploadPerformance: UploadPerformanceMock.veryQuick + ), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: 1, + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + let send2RequestsExpectation = expectation(description: "send 2 requests") + send2RequestsExpectation.expectedFulfillmentCount = 2 + + let requestBuilderSpy = FeatureRequestBuilderSpy() + requestBuilderSpy.onRequest = { _, _ in send2RequestsExpectation.fulfill() } + + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + + // When + let scope = core.scope(for: FeatureMock.self) + core.set(trackingConsent: .pending) + scope.eventWriteContext { context, writer in + XCTAssertEqual(context.trackingConsent, .pending) + writer.write(value: FeatureMock.Event(event: "pending")) + } + + core.set(trackingConsent: .granted) + scope.eventWriteContext { context, writer in + XCTAssertEqual(context.trackingConsent, .granted) + writer.write(value: FeatureMock.Event(event: "granted")) + } + + // Then + waitForExpectations(timeout: 2) + + let uploadedEvents = requestBuilderSpy.requestParameters + .flatMap { $0.events } + .map { $0.data.utf8String } + + XCTAssertEqual( + uploadedEvents, + [ + #"{"event":"pending"}"#, + #"{"event":"granted"}"# + ], + "It should upload all events" + ) + XCTAssertEqual(requestBuilderSpy.requestParameters.count, 2, "It should send 2 requests") + } + func testWhenWritingEventsWithBypassingConsent_itUploadsAllEvents() throws { // Given let core = DatadogCore( diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index 5e857d5e0c..d0ecde8792 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -36,20 +36,28 @@ internal class DatadogCoreProxy: DatadogCoreProtocol { @ReadWriteLock private var featureScopeInterceptors: [String: FeatureScopeInterceptor] = [:] - init(context: DatadogContext = .mockAny()) { - self.context = context - self.core = DatadogCore( - directory: temporaryCoreDirectory, - dateProvider: SystemDateProvider(), - initialConsent: context.trackingConsent, - performance: .mockAny(), - httpClient: HTTPClientMock(), - encryption: nil, - contextProvider: DatadogContextProvider(context: context), - applicationVersion: context.version, - maxBatchesPerUpload: .mockRandom(min: 1, max: 100), - backgroundTasksEnabled: .mockAny() + convenience init(context: DatadogContext = .mockAny()) { + self.init( + core: DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: context.trackingConsent, + performance: .mockAny(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: DatadogContextProvider( + context: context + ), + applicationVersion: context.version, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) ) + } + + init(core: DatadogCore) { + self.context = core.contextProvider.read() + self.core = core // override the message-bus's core instance core.bus.connect(core: self) diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift index bc20a3704a..7b8f9e4f97 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift @@ -29,15 +29,21 @@ internal class FeatureRequestBuilderMock: FeatureRequestBuilder { } internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { - /// Records parameters passed to `requet(for:with:)` + /// Stores the parameters passed to the `request(for:with:)` method. + @ReadWriteLock private(set) var requestParameters: [(events: [Event], context: DatadogContext)] = [] + /// A closure that is called when a request is about to be created in the `request(for:with:)` method. + @ReadWriteLock + var onRequest: ((_ events: [Event], _ context: DatadogContext) -> Void)? + func request( for events: [Event], with context: DatadogContext, execution: ExecutionContext ) throws -> URLRequest { requestParameters.append((events: events, context: context)) + onRequest?(events, context) return .mockAny() } } diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 30392a2d28..30163a4f16 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -256,6 +256,7 @@ extension RUMResourceEvent: RandomMockable { firstByte: .init(duration: .mockRandom(), start: .mockRandom()), id: .mockRandom(), method: .mockRandom(), + protocol: nil, provider: .init( domain: .mockRandom(), name: .mockRandom(), @@ -478,6 +479,7 @@ extension RUMLongTaskEvent: RandomMockable { isFrozenFrame: .mockRandom(), renderStart: nil, scripts: nil, + startTime: nil, styleAndLayoutStart: nil ), os: .mockRandom(), diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index ca56405fb8..19451de3cc 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -41,7 +41,7 @@ extension WebViewEventReceiver: AnyMockable { featureScope: FeatureScope = NOPFeatureScope(), dateProvider: DateProvider = SystemDateProvider(), commandSubscriber: RUMCommandSubscriber = RUMCommandSubscriberMock(), - viewCache: ViewCache = ViewCache() + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()) ) -> Self { .init( featureScope: featureScope, @@ -729,7 +729,7 @@ extension RUMScopeDependencies { syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), watchdogTermination: WatchdogTerminationMonitor? = nil diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift index 26df699b63..aaafdfc184 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift @@ -99,9 +99,9 @@ class VitalInfoSamplerTests: XCTestCase { DispatchQueue.global().sync { // in real-world scenarios, sampling will be started from background threads sampler = VitalInfoSampler( - cpuReader: VitalCPUReader(), + cpuReader: VitalCPUReader(notificationCenter: .default), memoryReader: VitalMemoryReader(), - refreshRateReader: VitalRefreshRateReader(), + refreshRateReader: VitalRefreshRateReader(notificationCenter: .default), frequency: 0.1 ) } diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index ae7d7fb263..94ac387268 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -41,7 +41,14 @@ class TracerTests: XCTestCase { source: "abc", sdkVersion: "1.2.3", ciAppOrigin: nil, - applicationBundleIdentifier: "com.datadoghq.ios-sdk" + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + device: .mockWith( + name: "iPhone", + model: "iPhone10,1", + osVersion: "15.4.1", + osBuildNumber: "13D20", + architecture: "arm64" + ) ) config.dateProvider = RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) config.traceIDGenerator = RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)) @@ -71,6 +78,15 @@ class TracerTests: XCTestCase { "type": "custom", "meta.tracer.version": "1.2.3", "meta.version": "1.0.0", + "meta.device.architecture": "arm64", + "meta.device.brand": "Apple", + "meta.device.model": "iPhone10,1", + "meta.device.name": "iPhone", + "meta.device.type": "mobile", + "meta.os.build": "13D20", + "meta.os.name": "iOS", + "meta.os.version": "15.4.1", + "meta.os.version_major": "15", "meta._dd.source": "abc", "metrics._top_level": 1, "metrics._sampling_priority_v1": 1, diff --git a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift index b58ecc9804..4c9fccc297 100644 --- a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift +++ b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift @@ -46,6 +46,11 @@ internal class RUMSessionMatcher { sessionEventMatchers: eventMatchers ) } + .sorted { session1, session2 in + let startTime1 = session1.views.first?.viewEvents.first?.date ?? 0 + let startTime2 = session2.views.first?.viewEvents.first?.date ?? 0 + return startTime1 < startTime2 + } } // MARK: - View Visits @@ -106,6 +111,21 @@ internal class RUMSessionMatcher { let errorEventMatchers: [RUMEventMatcher] let longTaskEventMatchers: [RUMEventMatcher] + /// `RUMView` events tracked in this session. + let viewEvents: [RUMViewEvent] + + /// `RUMAction` events tracked in this session. + let actionEvents: [RUMActionEvent] + + /// `RUMResource` events tracked in this session. + let resourceEvents: [RUMResourceEvent] + + /// `RUMError` events tracked in this session. + let errorEvents: [RUMErrorEvent] + + /// `RUMLongTask` events tracked in this session. + let longTaskEvents: [RUMLongTaskEvent] + private init(applicationID: String, sessionID: String, sessionEventMatchers: [RUMEventMatcher]) throws { // Sort events so they follow increasing time order let sessionEventOrderedByTime = try sessionEventMatchers.sorted { firstEvent, secondEvent in @@ -257,6 +277,11 @@ internal class RUMSessionMatcher { } self.views = visitsEventOrderedByTime + self.viewEvents = viewEvents + self.actionEvents = actionEvents + self.resourceEvents = resourceEvents + self.errorEvents = errorEvents + self.longTaskEvents = longTaskEvents } /// Checks if this session contains a view with a specific ID. @@ -424,6 +449,15 @@ extension Array where Element == RUMSessionMatcher { } return self[0] } + + /// Returns the only two sessions in this array. + /// Throws if there are more or less than 2 sessions in this array. + func takeTwo() throws -> (RUMSessionMatcher, RUMSessionMatcher) { + guard count == 2 else { + throw RUMSessionConsistencyException(description: "Expected 2 sessions, but found \(count)") + } + return (self[0], self[1]) + } } extension Array where Element == RUMSessionMatcher.View { @@ -498,79 +532,216 @@ extension RUMSessionMatcher { // MARK: - Debugging +extension RUMSessionMatcher.View { + /// The start of this view (as timestamp; milliseconds) defined as the start timestamp of the earliest view event in this view. + var startTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } +} + extension RUMSessionMatcher: CustomStringConvertible { - var description: String { - var description = "[🎞 RUM session (application.id: \(applicationID), session.id: \(sessionID), number of views: \(views.count))]" + var description: String { renderSession() } + + /// The start of this session (as timestamp; milliseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } + + /// The start of this session (as timestamp; nanoseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampNs: Int64 { sessionStartTimestampMs * 1_000_000 } + + /// The end of this session (as timestamp; nanoseconds) defined as the end timestamp of the latest view in this session. + private var sessionEndTimestampNs: Int64 { viewEvents.map({ $0.date * 1_000_000 + $0.view.timeSpent }).max() ?? 0 } + + private func renderSession() -> String { + var output = renderBox(string: "🎞 RUM session") + output += renderAttributesBox( + attributes: [ + ("application.id", applicationID), + ("id", sessionID), + ("views.count", "\(views.count)"), + ("start", prettyDate(timestampMs: sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: sessionEndTimestampNs - sessionStartTimestampNs)), + ] + ) views.forEach { view in - description += "\n\(describe(viewVisit: view))" + output += render(view: view) } - return description + output += renderClosingLine() + return output } - private func describe(viewVisit: View) -> String { - guard let lastViewEvent = viewVisit.viewEvents.last else { - return " → [⛔️ Invalid View - it has no view events]" + private func render(view: View) -> String { + guard let lastViewEvent = view.viewEvents.last else { + return renderBox(string: "⛔️ Invalid View - it has no view events") } - var description = " → [📸 View (name: '\(viewVisit.name ?? "nil")', id: \(viewVisit.viewID), duration: \(seconds(from: lastViewEvent.view.timeSpent)) actions.count: \(lastViewEvent.view.action.count), resources.count: \(lastViewEvent.view.resource.count), errors.count: \(lastViewEvent.view.error.count), longTask.count: \(lastViewEvent.view.longTask?.count ?? 0), frozenFrames.count: \(lastViewEvent.view.frozenFrame?.count ?? 0)]" + var output = renderBox(string: "📸 RUM View (\(view.name ?? "nil"))") + output += renderAttributesBox( + attributes: [ + ("name", view.name ?? "nil"), + ("id", view.viewID), + ("date", prettyDate(timestampMs: lastViewEvent.date)), + ("date (relative in session)", pretty(milliseconds: lastViewEvent.date - sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: lastViewEvent.view.timeSpent)), + ("event counts", "view (\(view.viewEvents.count)), action (\(view.actionEvents.count)), resource (\(view.resourceEvents.count)), error (\(view.errorEvents.count)), long task (\(view.longTaskEvents.count))"), + ] + ) - if !viewVisit.actionEvents.isEmpty { - description += "\n → action events:" - description += "\n\(describe(actionEvents: viewVisit.actionEvents))" + for action in view.actionEvents { + output += renderEmptyLine() + output += render(event: action, in: view) } - if !viewVisit.resourceEvents.isEmpty { - description += "\n → resource events:" - description += "\n\(describe(resourceEvents: viewVisit.resourceEvents))" + for resource in view.resourceEvents { + output += renderEmptyLine() + output += render(event: resource, in: view) } - if !viewVisit.errorEvents.isEmpty { - description += "\n → error events:" - description += "\n\(describe(errorEvents: viewVisit.errorEvents))" + for error in view.errorEvents { + output += renderEmptyLine() + output += render(event: error, in: view) } - if !viewVisit.longTaskEvents.isEmpty { - description += "\n → long task events:" - description += "\n\(describe(longTaskEvents: viewVisit.longTaskEvents))" + for longTask in view.longTaskEvents { + output += renderEmptyLine() + output += render(event: longTask, in: view) } - return description + output += renderEmptyLine() + return output + } + + private func render(event: RUMActionEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("▶️ RUM Action", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("name", event.action.target?.name ?? "nil"), + ("type", "\(event.action.type)"), + ("loading.time", "\(event.action.loadingTime.flatMap({ pretty(nanoseconds: $0) }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMResourceEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🌎 RUM Resource", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("url", event.resource.url), + ("method", "\(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "nil")"), + ("status.code", "\(event.resource.statusCode.flatMap({ "\($0)" }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMErrorEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🧯 RUM Error", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("message", event.error.message), + ("type", event.error.type ?? "nil"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMLongTaskEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🐌 RUM Long Task", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("duration", pretty(nanoseconds: event.longTask.duration)), + ], + prefix: "→", + indentationLevel: 3 + ) + return output } - private func describe(actionEvents: [RUMActionEvent]) -> String { - return actionEvents - .map { event in - " → [▶️ Action (name: \(event.action.target?.name ?? "(null)"), type: \(event.action.type)]" - } - .joined(separator: "\n") + // MARK: - Rendering helpers + + private static let rendererWidth = 90 + + private func renderBox(string: String) -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder1 = "+" + String(repeating: "-", count: width - 2) + "+" + let horizontalBorder2 = "|" + String(repeating: "-", count: width - 2) + "|" + let visualWidth = (string as NSString).length + let padding = (width - 2 - visualWidth) / 2 + let leftPadding = String(repeating: " ", count: max(0, padding)) + let rightPadding = String(repeating: " ", count: max(0, width - 2 - visualWidth - padding)) + + let contentLine = "|\(leftPadding)\(string)\(rightPadding)|" + + return """ + \(horizontalBorder1) + \(contentLine) + \(horizontalBorder2)\n + """ } - private func describe(resourceEvents: [RUMResourceEvent]) -> String { - return resourceEvents - .map { event in - " → [🌎 Resource (url: \(event.resource.url), method: \(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "(null)"), statusCode: \(event.resource.statusCode.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderAttributesBox(attributes: [(String, String)], prefix: String = "", indentationLevel: Int = 0) -> String { + let width = RUMSessionMatcher.rendererWidth + let indentation = String(repeating: " ", count: indentationLevel) + + let contentLines = attributes.map { key, value in + let lineContent = "\(indentation)\(prefix) \(key): \(value)" + let visualWidth = (lineContent as NSString).length + let padding = max(0, width - 2 - visualWidth) + let rightPadding = String(repeating: " ", count: padding) + return "|\(lineContent)\(rightPadding)|" + } + + return """ + \(contentLines.joined(separator: "\n"))\n + """ } - private func describe(errorEvents: [RUMErrorEvent]) -> String { - return errorEvents - .map { event in - " → [🧯 Error (message: \(event.error.message), type: \(event.error.type ?? "(null)"), resource: \(event.error.resource.flatMap({ "\($0.url)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderEmptyLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "|" + String(repeating: " ", count: width - 2) + "|" + return horizontalBorder + "\n" } - private func describe(longTaskEvents: [RUMLongTaskEvent]) -> String { - return longTaskEvents - .map { event in - " → [🐌 LongTask (duration: \(seconds(from: event.longTask.duration)), isFrozenFrame: \(event.longTask.isFrozenFrame.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderClosingLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "+" + String(repeating: "-", count: width - 2) + "+" + return horizontalBorder + "\n" + } + + private func pretty(milliseconds: Int64) -> String { + pretty(nanoseconds: milliseconds * 1_000_000) } - private func seconds(from nanoseconds: Int64) -> String { - let prettySeconds = (round((Double(nanoseconds) / 1_000_000_000) * 100)) / 100 - return "\(prettySeconds)s" + private func pretty(nanoseconds: Int64) -> String { + if nanoseconds >= 1_000_000_000 { + let seconds = round((Double(nanoseconds) / 1_000_000_000) * 100) / 100 + return "\(seconds)s" + } else if nanoseconds >= 1_000_000 { + let milliseconds = round((Double(nanoseconds) / 1_000_000) * 100) / 100 + return "\(milliseconds)ms" + } else { + return "\(nanoseconds)ns" + } + } + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter + }() + + private func prettyDate(timestampMs: Int64) -> String { + let timestampSec = TimeInterval(timestampMs) / 1_000 + let date = Date(timeIntervalSince1970: timestampSec) + return RUMSessionMatcher.dateFormatter.string(from: date) } } diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec index da33eb1937..01c3ef9eb8 100644 --- a/DatadogCrashReporting.podspec +++ b/DatadogCrashReporting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCrashReporting" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index 6373cebfbc..fcc8652a86 100644 --- a/DatadogInternal.podspec +++ b/DatadogInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogInternal" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Datadog Internal Package. This module is not for public use." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift index 79923d6118..271d257eab 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -6,6 +6,15 @@ import Foundation +/// A protocol that provides access to the current application state. +/// See: https://developer.apple.com/documentation/uikit/uiapplication/state +public protocol AppStateProvider: Sendable { + /// The current application state. + /// + /// **Note**: Must be called on the main thread. + var current: AppState { get } +} + /// Application state. public enum AppState: Codable, PassthroughAnyCodable { /// The app is running in the foreground and currently receiving events. @@ -142,31 +151,66 @@ extension AppStateHistory { } } -#if canImport(UIKit) - -import UIKit - #if canImport(WatchKit) + import WatchKit -public typealias ApplicationState = WKApplicationState -#else -public typealias ApplicationState = UIApplication.State -#endif +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let wkState = WKExtension.dd.shared.applicationState + return AppState(wkState) + } +} extension AppState { - public init(_ state: ApplicationState) { + public init(_ state: WKApplicationState) { switch state { - case .active: - self = .active - case .inactive: - self = .inactive - case .background: - self = .background + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background @unknown default: - self = .active // in case a new state is introduced, we rather want to fallback to most expected state + self = .active // in case a new state is introduced, default to most expected state } } } +#elseif canImport(UIKit) + +import UIKit + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state + return AppState(uiKitState) + } +} + +extension AppState { + public init(_ state: UIApplication.State) { + switch state { + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background + @unknown default: self = .active // in case a new state is introduced, default to most expected state + } + } +} + +#else // macOS (no UIKit and no WatchKit) + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + public let current: AppState = .active +} + #endif diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index 9c63c2463f..9dd865b880 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -8,6 +8,15 @@ import Foundation /// Describes current device information. public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { + /// Represents the type of device. + public enum DeviceType: Codable, Equatable, PassthroughAnyCodable { + case iPhone + case iPod + case iPad + case appleTV + case other(modelName: String, osName: String) + } + // MARK: - Info /// Device manufacturer name. Always'Apple' @@ -19,12 +28,18 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { /// Device model name, e.g. "iPhone10,1", "iPhone13,2". public let model: String + /// The type of device. + public let type: DeviceType + /// The name of operating system, e.g. "iOS", "iPadOS", "tvOS". public let osName: String /// The version of the operating system, e.g. "15.4.1". public let osVersion: String + /// The major version of the operating system, e.g. "15". + public let osVersionMajor: String + /// The build numer of the operating system, e.g. "15D21" or "13D20". public let osBuildNumber: String? @@ -58,8 +73,10 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { self.brand = "Apple" self.name = name self.model = model + self.type = DeviceType(modelName: model, osName: osName) self.osName = osName self.osVersion = osVersion + self.osVersionMajor = osVersion.split(separator: ".").first.map { String($0) } ?? osVersion self.osBuildNumber = osBuildNumber self.architecture = architecture self.isSimulator = isSimulator @@ -69,6 +86,29 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { } } +private extension DeviceInfo.DeviceType { + /// Infers `DeviceType` from provided model name and operating system name. + /// - Parameters: + /// - modelName: The name of the device model, e.g. "iPhone10,1". + /// - osName: The name of the operating system, e.g. "iOS", "tvOS". + init(modelName: String, osName: String) { + let lowercasedModelName = modelName.lowercased() + let lowercasedOSName = osName.lowercased() + + if lowercasedModelName.hasPrefix("iphone") { + self = .iPhone + } else if lowercasedModelName.hasPrefix("ipod") { + self = .iPod + } else if lowercasedModelName.hasPrefix("ipad") { + self = .iPad + } else if lowercasedModelName.hasPrefix("appletv") || lowercasedOSName == "tvos" { + self = .appleTV + } else { + self = .other(modelName: modelName, osName: osName) + } + } +} + import MachO #if canImport(UIKit) @@ -81,7 +121,7 @@ extension DeviceInfo { /// - processInfo: The current process information. /// - device: The device description. public init( - processInfo: ProcessInfo = .processInfo, + processInfo: ProcessInfo, device: _UIDevice = .dd.current, sysctl: SysctlProviding = Sysctl() ) { diff --git a/DatadogInternal/Tests/Context/DeviceInfoTests.swift b/DatadogInternal/Tests/Context/DeviceInfoTests.swift index 8c5e2c1af5..a2b7f297ee 100644 --- a/DatadogInternal/Tests/Context/DeviceInfoTests.swift +++ b/DatadogInternal/Tests/Context/DeviceInfoTests.swift @@ -32,4 +32,46 @@ class DeviceInfoTests: XCTestCase { XCTAssertEqual(info.osVersion, randomOSVersion) XCTAssertNotNil(info.osBuildNumber) } + + func testDeviceType() { + // Given + let iPhone = UIDeviceMock(model: "iPhone14,5", systemName: "iOS") + let iPod = UIDeviceMock(model: "iPod7,1", systemName: "iOS") + let iPad = UIDeviceMock(model: "iPad12,1", systemName: "iPadOS") + let appleTV1 = UIDeviceMock(model: "J305AP", systemName: "tvOS") + let appleTV2 = UIDeviceMock(model: "AppleTV14,1 Simulator", systemName: "tvOS") + let other = UIDeviceMock(model: "RealityDevice14,1", systemName: "visionOS") + + // When / Then + func when(device: UIDeviceMock) -> DeviceInfo { + return DeviceInfo(processInfo: ProcessInfoMock(), device: device) + } + + XCTAssertEqual(when(device: iPhone).type, .iPhone) + XCTAssertEqual(when(device: iPod).type, .iPod) + XCTAssertEqual(when(device: iPad).type, .iPad) + XCTAssertEqual(when(device: appleTV1).type, .appleTV) + XCTAssertEqual(when(device: appleTV2).type, .appleTV) + XCTAssertEqual(when(device: other).type, .other(modelName: "RealityDevice14,1 Simulator", osName: "visionOS")) + } + + func testOSVersionMajor() { + // When + func when(systemVersion: String) -> DeviceInfo { + return DeviceInfo( + processInfo: ProcessInfoMock(), + device: UIDeviceMock(systemVersion: systemVersion) + ) + } + + // Then + XCTAssertEqual(when(systemVersion: "15.4.1").osVersion, "15.4.1") + XCTAssertEqual(when(systemVersion: "15.4.1").osVersionMajor, "15") + + XCTAssertEqual(when(systemVersion: "17.0").osVersion, "17.0") + XCTAssertEqual(when(systemVersion: "17.0").osVersionMajor, "17") + + XCTAssertEqual(when(systemVersion: "18").osVersion, "18") + XCTAssertEqual(when(systemVersion: "18").osVersionMajor, "18") + } } diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index af1584f205..ab37ab415a 100644 --- a/DatadogLogs.podspec +++ b/DatadogLogs.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogLogs" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Datadog Logs Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogObjc.podspec b/DatadogObjc.podspec index 25bddfccba..27d32c0b28 100644 --- a/DatadogObjc.podspec +++ b/DatadogObjc.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogObjc" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 9c6bf5d2dd..e8d81c417d 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -713,7 +713,8 @@ public class DDRUMActionEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -963,7 +964,8 @@ public class DDRUMActionEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -1502,7 +1504,8 @@ public class DDRUMErrorEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -2181,7 +2184,8 @@ public class DDRUMErrorEventFeatureFlags: NSObject { } @objc public var featureFlagsInfo: [String: Any] { - root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } } } @@ -2350,7 +2354,8 @@ public class DDRUMErrorEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -2885,7 +2890,8 @@ public class DDRUMLongTaskEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -3023,6 +3029,10 @@ public class DDRUMLongTaskEventLongTask: NSObject { root.swiftModel.longTask.scripts?.map { DDRUMLongTaskEventLongTaskScripts(swiftModel: $0) } } + @objc public var startTime: NSNumber? { + root.swiftModel.longTask.startTime as NSNumber? + } + @objc public var styleAndLayoutStart: NSNumber? { root.swiftModel.longTask.styleAndLayoutStart as NSNumber? } @@ -3292,7 +3302,8 @@ public class DDRUMLongTaskEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -3835,7 +3846,8 @@ public class DDRUMResourceEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -4006,6 +4018,10 @@ public class DDRUMResourceEventResource: NSObject { .init(swift: root.swiftModel.resource.method) } + @objc public var `protocol`: String? { + root.swiftModel.resource.protocol + } + @objc public var provider: DDRUMResourceEventResourceProvider? { root.swiftModel.resource.provider != nil ? DDRUMResourceEventResourceProvider(root: root) : nil } @@ -4517,7 +4533,8 @@ public class DDRUMResourceEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -5092,7 +5109,8 @@ public class DDRUMViewEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -5228,7 +5246,8 @@ public class DDRUMViewEventFeatureFlags: NSObject { } @objc public var featureFlagsInfo: [String: Any] { - root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } } } @@ -5428,7 +5447,8 @@ public class DDRUMViewEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -6317,7 +6337,8 @@ public class DDRUMVitalEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.dd.objCAttributes + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -6567,7 +6588,8 @@ public class DDRUMVitalEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.dd.objCAttributes + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -6827,7 +6849,8 @@ public class DDTelemetryErrorEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -7077,7 +7100,8 @@ public class DDTelemetryDebugEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -7306,7 +7330,8 @@ public class DDTelemetryConfigurationEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -7397,7 +7422,8 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { } @objc public var plugins: [DDTelemetryConfigurationEventTelemetryConfigurationPlugins]? { - root.swiftModel.telemetry.configuration.plugins?.map { DDTelemetryConfigurationEventTelemetryConfigurationPlugins(swiftModel: $0) } + set { root.swiftModel.telemetry.configuration.plugins = newValue?.map { $0.swiftModel } } + get { root.swiftModel.telemetry.configuration.plugins?.map { DDTelemetryConfigurationEventTelemetryConfigurationPlugins(swiftModel: $0) } } } @objc public var premiumSampleRate: NSNumber? { @@ -7698,7 +7724,8 @@ public class DDTelemetryConfigurationEventTelemetryConfigurationPlugins: NSObjec } @objc public var pluginsInfo: [String: Any] { - root.swiftModel.pluginsInfo.dd.objCAttributes + set { root.swiftModel.pluginsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.pluginsInfo.dd.objCAttributes } } } @@ -7866,4 +7893,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/ec07c062cbbb2f19b49d08f72bc95703b502906d +// Generated from https://github.com/DataDog/rum-events-format/tree/e1c6dde3793714453b5b49f17790a24e9ff9b77b diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec index a6824d4fb6..e171414076 100644 --- a/DatadogRUM.podspec +++ b/DatadogRUM.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogRUM" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Datadog Real User Monitoring Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index 3691352a0a..4826b45391 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -37,7 +37,7 @@ public struct RUMActionEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -67,7 +67,7 @@ public struct RUMActionEvent: RUMDataModel { public let type: String = "action" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -480,7 +480,7 @@ public struct RUMErrorEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -495,7 +495,7 @@ public struct RUMErrorEvent: RUMDataModel { public var error: Error /// Feature flags properties - public internal(set) var featureFlags: FeatureFlags? + public var featureFlags: FeatureFlags? /// Properties of App Hang and ANR errors public let freeze: Freeze? @@ -519,7 +519,7 @@ public struct RUMErrorEvent: RUMDataModel { public let type: String = "error" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -1008,7 +1008,7 @@ public struct RUMErrorEvent: RUMDataModel { /// Feature flags properties public struct FeatureFlags: Codable { - public internal(set) var featureFlagsInfo: [String: Encodable] + public var featureFlagsInfo: [String: Encodable] } /// Properties of App Hang and ANR errors @@ -1129,7 +1129,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -1162,7 +1162,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let type: String = "long_task" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -1361,6 +1361,9 @@ public struct RUMLongTaskEvent: RUMDataModel { /// A list of long scripts that were executed over the course of the long frame public let scripts: [Scripts]? + /// Start time of the long animation frame + public let startTime: Double? + /// Start time of the time period spent in style and layout calculations public let styleAndLayoutStart: Double? @@ -1373,6 +1376,7 @@ public struct RUMLongTaskEvent: RUMDataModel { case isFrozenFrame = "is_frozen_frame" case renderStart = "render_start" case scripts = "scripts" + case startTime = "start_time" case styleAndLayoutStart = "style_and_layout_start" } @@ -1522,7 +1526,7 @@ public struct RUMResourceEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -1555,7 +1559,7 @@ public struct RUMResourceEvent: RUMDataModel { public let type: String = "resource" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -1772,6 +1776,9 @@ public struct RUMResourceEvent: RUMDataModel { /// HTTP method of the resource public let method: RUMMethod? + /// Network protocol used to fetch the resource (e.g., 'http/1.1', 'h2') + public let `protocol`: String? + /// The provider for this resource public let provider: Provider? @@ -1810,6 +1817,7 @@ public struct RUMResourceEvent: RUMDataModel { case graphql = "graphql" case id = "id" case method = "method" + case `protocol` = "protocol" case provider = "provider" case redirect = "redirect" case renderBlockingStatus = "render_blocking_status" @@ -2069,7 +2077,7 @@ public struct RUMViewEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -2081,7 +2089,7 @@ public struct RUMViewEvent: RUMDataModel { public let display: Display? /// Feature flags properties - public internal(set) var featureFlags: FeatureFlags? + public var featureFlags: FeatureFlags? /// Operating system properties public let os: RUMOperatingSystem? @@ -2105,7 +2113,7 @@ public struct RUMViewEvent: RUMDataModel { public let type: String = "view" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -2349,7 +2357,7 @@ public struct RUMViewEvent: RUMDataModel { /// Feature flags properties public struct FeatureFlags: Codable { - public internal(set) var featureFlagsInfo: [String: Encodable] + public var featureFlagsInfo: [String: Encodable] } /// Privacy properties @@ -2800,7 +2808,7 @@ public struct RUMVitalEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -2830,7 +2838,7 @@ public struct RUMVitalEvent: RUMDataModel { public let type: String = "vital" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -3120,7 +3128,7 @@ public struct TelemetryErrorEvent: RUMDataModel { public let source: Source /// The telemetry log information - public internal(set) var telemetry: Telemetry + public var telemetry: Telemetry /// Telemetry event type. Should specify telemetry only. public let type: String = "telemetry" @@ -3217,7 +3225,7 @@ public struct TelemetryErrorEvent: RUMDataModel { /// Telemetry type public let type: String? = "log" - public internal(set) var telemetryInfo: [String: Encodable] + public var telemetryInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case device = "device" @@ -3322,7 +3330,7 @@ public struct TelemetryDebugEvent: RUMDataModel { public let source: Source /// The telemetry log information - public internal(set) var telemetry: Telemetry + public var telemetry: Telemetry /// Telemetry event type. Should specify telemetry only. public let type: String = "telemetry" @@ -3416,7 +3424,7 @@ public struct TelemetryDebugEvent: RUMDataModel { /// Telemetry type public let type: String? = "log" - public internal(set) var telemetryInfo: [String: Encodable] + public var telemetryInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case device = "device" @@ -3595,7 +3603,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// Telemetry type public let type: String = "configuration" - public internal(set) var telemetryInfo: [String: Encodable] + public var telemetryInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case configuration = "configuration" @@ -3661,7 +3669,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { public var mobileVitalsUpdatePeriod: Int64? /// The list of plugins enabled - public internal(set) var plugins: [Plugins]? + public var plugins: [Plugins]? /// The percentage of sessions with Browser RUM & Session Replay pricing tracked (deprecated in favor of session_replay_sample_rate) public let premiumSampleRate: Int64? @@ -3981,7 +3989,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// The name of the plugin public let name: String - public internal(set) var pluginsInfo: [String: Encodable] + public var pluginsInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case name = "name" @@ -4127,7 +4135,7 @@ public struct TelemetryUsageEvent: RUMDataModel { public let source: Source /// The telemetry usage information - public internal(set) var telemetry: Telemetry + public var telemetry: Telemetry /// Telemetry event type. Should specify telemetry only. public let type: String = "telemetry" @@ -4217,7 +4225,7 @@ public struct TelemetryUsageEvent: RUMDataModel { public let usage: Usage - public internal(set) var telemetryInfo: [String: Encodable] + public var telemetryInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case device = "device" @@ -4626,7 +4634,7 @@ public struct RUMConnectivity: Codable { /// User provided context public struct RUMEventAttributes: Codable { - public internal(set) var contextInfo: [String: Encodable] + public var contextInfo: [String: Encodable] } extension RUMEventAttributes { @@ -4748,7 +4756,7 @@ public struct RUMUser: Codable { /// Name of the user public let name: String? - public internal(set) var usrInfo: [String: Encodable] + public var usrInfo: [String: Encodable] enum StaticCodingKeys: String, CodingKey { case email = "email" @@ -4885,4 +4893,4 @@ public struct RUMTelemetryOperatingSystem: Codable { } } -// Generated from https://github.com/DataDog/rum-events-format/tree/ec07c062cbbb2f19b49d08f72bc95703b502906d +// Generated from https://github.com/DataDog/rum-events-format/tree/e1c6dde3793714453b5b49f17790a24e9ff9b77b diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 24edb64a6a..401054cc3b 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -54,7 +54,7 @@ internal final class RUMFeature: DatadogRemoteFeature { appStateManager: appStateManager, featureScope: featureScope ), - stroage: core.storage, + storage: core.storage, feature: featureScope, reporter: WatchdogTerminationReporter( featureScope: featureScope, @@ -100,7 +100,7 @@ internal final class RUMFeature: DatadogRemoteFeature { ) }, onSessionStart: configuration.onSessionStart, - viewCache: ViewCache(), + viewCache: ViewCache(dateProvider: configuration.dateProvider), fatalErrorContext: FatalErrorContextNotifier(messageBus: featureScope), sessionEndedMetric: sessionEndedMetric, watchdogTermination: watchdogTermination @@ -114,7 +114,8 @@ internal final class RUMFeature: DatadogRemoteFeature { let memoryWarningReporter = MemoryWarningReporter() let memoryWarningMonitor = MemoryWarningMonitor( backtraceReporter: core.backtraceReporter, - memoryWarningReporter: memoryWarningReporter + memoryWarningReporter: memoryWarningReporter, + notificationCenter: configuration.notificationCenter ) self.instrumentation = RUMInstrumentation( @@ -128,6 +129,7 @@ internal final class RUMFeature: DatadogRemoteFeature { backtraceReporter: core.backtraceReporter, fatalErrorContext: dependencies.fatalErrorContext, processID: configuration.processID, + notificationCenter: configuration.notificationCenter, watchdogTermination: watchdogTermination, memoryWarningMonitor: memoryWarningMonitor ) diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift index 06aa9ac492..1f3c77a70d 100644 --- a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift @@ -17,7 +17,7 @@ internal final class MemoryWarningMonitor { init( backtraceReporter: BacktraceReporting?, memoryWarningReporter: MemoryWarningReporting, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter ) { self.notificationCenter = notificationCenter self.backtraceReporter = backtraceReporter diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 3324111a3f..8eb800bedf 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -55,12 +55,17 @@ internal final class RUMInstrumentation: RUMCommandPublisher { backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, processID: UUID, + notificationCenter: NotificationCenter, watchdogTermination: WatchdogTerminationMonitor?, memoryWarningMonitor: MemoryWarningMonitor ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) // and only swizzle `UIViewController` if UIKit instrumentation is configured: - let viewsHandler = RUMViewsHandler(dateProvider: dateProvider, predicate: uiKitRUMViewsPredicate) + let viewsHandler = RUMViewsHandler( + dateProvider: dateProvider, + predicate: uiKitRUMViewsPredicate, + notificationCenter: notificationCenter + ) let viewControllerSwizzler: UIViewControllerSwizzler? = { do { if uiKitRUMViewsPredicate != nil { diff --git a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift index 6de74549ea..01ad47bb74 100644 --- a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift @@ -65,7 +65,7 @@ internal final class RUMViewsHandler { init( dateProvider: DateProvider, predicate: UIKitRUMViewsPredicate?, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter ) { self.dateProvider = dateProvider self.predicate = predicate diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift index 9c9eb66b8f..0de34f3f90 100644 --- a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift @@ -44,7 +44,7 @@ internal final class WatchdogTerminationMonitor { init( appStateManager: WatchdogTerminationAppStateManager, checker: WatchdogTerminationChecker, - stroage: Storage?, + storage: Storage?, feature: FeatureScope, reporter: WatchdogTerminationReporting ) { @@ -52,7 +52,7 @@ internal final class WatchdogTerminationMonitor { self.appStateManager = appStateManager self.feature = feature self.reporter = reporter - self.storage = stroage + self.storage = storage self.currentState = .stopped } diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index e192539f50..79f1f70f70 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -279,6 +279,8 @@ extension RUM { internal var mainQueue: DispatchQueue = .main /// Identifier of the current process, used to check if fatal App Hang originated in a previous process instance. internal var processID: UUID = currentProcessID + /// The default notification center used for subscribing to app lifecycle events and system notifications. + internal var notificationCenter: NotificationCenter = .default internal var debugSDK: Bool = ProcessInfo.processInfo.arguments.contains(LaunchArguments.Debug) internal var debugViews: Bool = ProcessInfo.processInfo.arguments.contains("DD_DEBUG_RUM") diff --git a/DatadogRUM/Sources/RUMEvent/RUMDeviceInfo.swift b/DatadogRUM/Sources/RUMEvent/RUMDeviceInfo.swift index dd6e21dafd..d58d975912 100644 --- a/DatadogRUM/Sources/RUMEvent/RUMDeviceInfo.swift +++ b/DatadogRUM/Sources/RUMEvent/RUMDeviceInfo.swift @@ -28,29 +28,15 @@ extension RUMDevice { model: device.model, name: device.name, type: { - if let type = RUMDeviceType(from: device.model) { - return type - } else { - telemetry.debug("Couldn't read `RUMDeviceType` from `device.model`: \(device.model)") + switch device.type { + case .iPhone, .iPod: return .mobile + case .iPad: return .tablet + case .appleTV: return .tv + case .other(modelName: let modelName, osName: let osName): + telemetry.debug("Failed to map `device.model`: \(modelName) and `os.name`: \(osName) to any `RUMDeviceType`") return .other } }() ) } } - -private extension RUMDevice.RUMDeviceType { - init?(from deviceModel: String) { - let lowercasedModel = deviceModel.lowercased() - - if lowercasedModel.hasPrefix("iphone") || lowercasedModel.hasPrefix("ipod") { - self = .mobile - } else if lowercasedModel.hasPrefix("ipad") { - self = .tablet - } else if lowercasedModel.hasPrefix("appletv") { - self = .tv - } else { - return nil - } - } -} diff --git a/DatadogRUM/Sources/RUMEvent/RUMOperatingSystemInfo.swift b/DatadogRUM/Sources/RUMEvent/RUMOperatingSystemInfo.swift index 281afed501..597b8148bb 100644 --- a/DatadogRUM/Sources/RUMEvent/RUMOperatingSystemInfo.swift +++ b/DatadogRUM/Sources/RUMEvent/RUMOperatingSystemInfo.swift @@ -8,14 +8,10 @@ import Foundation import DatadogInternal extension RUMOperatingSystem { - init(context: DatadogContext) { - self.init(device: context.device) - } - init(device: DeviceInfo) { self.name = device.osName self.version = device.osVersion self.build = device.osBuildNumber - self.versionMajor = device.osVersion.split(separator: ".").first.map { String($0) } ?? device.osVersion + self.versionMajor = device.osVersionMajor } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift index 6df9469254..37b00917f3 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMResourceScope.swift @@ -188,7 +188,7 @@ internal class RUMResourceScope: RUMScope { date: resourceStartTime.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds, device: .init(context: context, telemetry: dependencies.telemetry), display: nil, - os: .init(context: context), + os: .init(device: context.device), resource: .init( connect: resourceMetrics?.connect.map { metric in .init( @@ -220,6 +220,7 @@ internal class RUMResourceScope: RUMScope { graphql: graphql, id: resourceUUID.toRUMDataFormat, method: resourceHTTPMethod, + protocol: nil, provider: resourceEventProvider, redirect: resourceMetrics?.redirection.map { metric in .init( @@ -320,7 +321,7 @@ internal class RUMResourceScope: RUMScope { wasTruncated: nil ), freeze: nil, - os: .init(context: context), + os: .init(device: context.device), service: context.service, session: .init( hasReplay: context.hasReplay, diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift index 807b735908..68449d2d5e 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift @@ -19,9 +19,9 @@ internal struct VitalsReaders { telemetry: Telemetry = NOPTelemetry() ) { self.frequency = frequency - self.cpu = VitalCPUReader(telemetry: telemetry) + self.cpu = VitalCPUReader(notificationCenter: .default, telemetry: telemetry) self.memory = VitalMemoryReader() - self.refreshRate = VitalRefreshRateReader() + self.refreshRate = VitalRefreshRateReader(notificationCenter: .default) } } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMUserActionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMUserActionScope.swift index 0a722957cf..572964ad58 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMUserActionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMUserActionScope.swift @@ -174,7 +174,7 @@ internal class RUMUserActionScope: RUMScope, RUMContextProvider { date: actionStartTime.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds, device: .init(context: context, telemetry: dependencies.telemetry), display: nil, - os: .init(context: context), + os: .init(device: context.device), service: context.service, session: .init( hasReplay: context.hasReplay, diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index 9749797eb6..21c586648b 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -434,7 +434,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { date: viewStartTime.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds, device: .init(context: context, telemetry: dependencies.telemetry), display: nil, - os: .init(context: context), + os: .init(device: context.device), service: context.service, session: .init( hasReplay: context.hasReplay, @@ -511,7 +511,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { device: .init(context: context, telemetry: dependencies.telemetry), display: nil, featureFlags: .init(featureFlagsInfo: featureFlags), - os: .init(context: context), + os: .init(device: context.device), privacy: nil, service: context.service, session: .init( @@ -663,7 +663,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { freeze: (command as? RUMAddCurrentViewAppHangCommand).map { appHangCommand in .init(duration: appHangCommand.hangDuration.toInt64Nanoseconds) }, - os: .init(context: context), + os: .init(device: context.device), service: context.service, session: .init( hasReplay: context.hasReplay, @@ -727,9 +727,10 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { isFrozenFrame: isFrozenFrame, renderStart: nil, scripts: nil, + startTime: nil, styleAndLayoutStart: nil ), - os: .init(context: context), + os: .init(device: context.device), service: context.service, session: .init( hasReplay: context.hasReplay, diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift index b002167585..ee72d56d80 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift @@ -32,7 +32,7 @@ internal final class ViewCache { /// - ttl: The TTL of view ids in cache. /// - capacity: The maximum number of ids to store. init( - dateProvider: DateProvider = SystemDateProvider(), + dateProvider: DateProvider, ttl: TimeInterval = 3.minutes, capacity: Int = 30 ) { diff --git a/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift b/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift index b326bea350..76e89ecd8d 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift @@ -18,7 +18,7 @@ internal class VitalCPUReader: SamplingBasedVitalReader { private let telemetry: Telemetry init( - notificationCenter: NotificationCenter = .default, + notificationCenter: NotificationCenter, telemetry: Telemetry = NOPTelemetry() ) { self.telemetry = telemetry diff --git a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift index 900ca922ee..67287a1054 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift @@ -17,7 +17,7 @@ internal class VitalRefreshRateReader: ContinuousVitalReader { private var nextFrameDuration: CFTimeInterval? private let notificationCenter: NotificationCenter - init(notificationCenter: NotificationCenter = .default) { + init(notificationCenter: NotificationCenter) { self.notificationCenter = notificationCenter notificationCenter.addObserver( diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 844c6a32c8..384c03147f 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -25,6 +25,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -52,6 +53,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -76,6 +78,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -103,6 +106,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -126,6 +130,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -149,6 +154,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -172,6 +178,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) diff --git a/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift index f40a875664..0bbfe09ea1 100644 --- a/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift @@ -221,7 +221,7 @@ class RUMViewsHandlerTests: XCTestCase { } } let predicate = Predicate() - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) // Given let someView = createMockViewInWindow() @@ -257,7 +257,7 @@ class RUMViewsHandlerTests: XCTestCase { let untrackedModal = createMockViewInWindow() let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -292,7 +292,7 @@ class RUMViewsHandlerTests: XCTestCase { let untrackedModal = createMockViewInWindow() let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -332,7 +332,7 @@ class RUMViewsHandlerTests: XCTestCase { untrackedModal.isModalInPresentation = true let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -372,7 +372,7 @@ class RUMViewsHandlerTests: XCTestCase { untrackedModal.isModalInPresentation = true let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift index 30acf3c4e6..af939fa6d5 100644 --- a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift @@ -103,7 +103,7 @@ extension WatchdogTerminationMonitor: RandomMockable { return .init( appStateManager: .mockRandom(), checker: .mockRandom(), - stroage: NOPDatadogCore().storage, + storage: NOPDatadogCore().storage, feature: FeatureScopeMock(), reporter: WatchdogTerminationReporter.mockRandom() ) diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift index ab3ac7fccf..252afc9494 100644 --- a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift @@ -111,7 +111,7 @@ final class WatchdogTerminationMonitorTests: XCTestCase { sut = WatchdogTerminationMonitor( appStateManager: appStateManager, checker: checker, - stroage: NOPDatadogCore().storage, + storage: NOPDatadogCore().storage, feature: featureScope, reporter: reporter ) diff --git a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift index c740f49d7e..abfa1cc8a1 100644 --- a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift +++ b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift @@ -195,7 +195,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(now: .mockDecember15th2019At10AMUTC()), commandSubscriber: commandsSubscriberMock, - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -214,7 +214,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -290,7 +290,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -317,7 +317,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index 0bebe1f8a2..b0e37fe624 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -274,6 +274,7 @@ extension RUMResourceEvent: RandomMockable { firstByte: .init(duration: .mockRandom(), start: .mockRandom()), id: .mockRandom(), method: .mockRandom(), + protocol: nil, provider: .init( domain: .mockRandom(), name: .mockRandom(), @@ -496,6 +497,7 @@ extension RUMLongTaskEvent: RandomMockable { isFrozenFrame: .mockRandom(), renderStart: .mockRandom(), scripts: nil, + startTime: nil, styleAndLayoutStart: nil ), os: .mockRandom(), diff --git a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift index 5f7bef6676..4c8adfa8da 100644 --- a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift @@ -773,7 +773,7 @@ extension RUMScopeDependencies { syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), watchdogTermination: WatchdogTerminationMonitor = .mockRandom() diff --git a/DatadogRUM/Tests/RUMEvent/RUMDeviceInfoTests.swift b/DatadogRUM/Tests/RUMEvent/RUMDeviceInfoTests.swift index fbd3dc8bf6..f1e9b64700 100644 --- a/DatadogRUM/Tests/RUMEvent/RUMDeviceInfoTests.swift +++ b/DatadogRUM/Tests/RUMEvent/RUMDeviceInfoTests.swift @@ -5,6 +5,7 @@ */ import XCTest +import DatadogInternal import TestUtilities @testable import DatadogRUM @@ -26,19 +27,19 @@ class RUMDeviceInfoTests: XCTestCase { func testItInfersDeviceTypeFromDeviceModel() { let iPhone = RUMDevice( - device: .mockWith(model: "iPhone" + String.mockRandom(among: .alphanumerics, length: 2)) + context: .mockWith(device: .mockWith(model: "iPhone" + String.mockRandom(among: .alphanumerics, length: 2))) ) let iPod = RUMDevice( - device: .mockWith(model: "iPod" + String.mockRandom(among: .alphanumerics, length: 2)) + context: .mockWith(device: .mockWith(model: "iPod" + String.mockRandom(among: .alphanumerics, length: 2))) ) let iPad = RUMDevice( - device: .mockWith(model: "iPad" + String.mockRandom(among: .alphanumerics, length: 2)) + context: .mockWith(device: .mockWith(model: "iPad" + String.mockRandom(among: .alphanumerics, length: 2))) ) let appleTV = RUMDevice( - device: .mockWith(model: "AppleTV" + String.mockRandom(among: .alphanumerics, length: 2)) + context: .mockWith(device: .mockWith(model: "AppleTV" + String.mockRandom(among: .alphanumerics, length: 2))) ) let unknownDevice = RUMDevice( - device: .mockWith(model: .mockRandom()) + context: .mockWith(device: .mockWith(model: .mockRandom())) ) XCTAssertEqual(iPhone.type, .mobile) diff --git a/DatadogRUM/Tests/RUMEvent/RUMOperatingSystemInfoTests.swift b/DatadogRUM/Tests/RUMEvent/RUMOperatingSystemInfoTests.swift deleted file mode 100644 index 4710076694..0000000000 --- a/DatadogRUM/Tests/RUMEvent/RUMOperatingSystemInfoTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-Present Datadog, Inc. - */ - -import XCTest -@testable import DatadogRUM - -class RUMOperatingSystemInfoTests: XCTestCase { - func testItSetsOSNameAndVersion() { - let randomOSName: String = .mockRandom() - let randomOSVersion: String = .mockRandom() - let randomOSBuild: String = .mockRandom() - - let info = RUMOperatingSystem( - device: .mockWith( - osName: randomOSName, - osVersion: randomOSVersion, - osBuildNumber: randomOSBuild - ) - ) - - XCTAssertEqual(info.name, randomOSName) - XCTAssertEqual(info.version, randomOSVersion) - XCTAssertEqual(info.build, randomOSBuild) - } - - func testItInfersOSMajorVersion() { - var info = RUMOperatingSystem(device: .mockWith(osVersion: "15.4.1")) - XCTAssertEqual(info.versionMajor, "15") - - info = RUMOperatingSystem(device: .mockWith(osVersion: "1.4")) - XCTAssertEqual(info.versionMajor, "1") - - info = RUMOperatingSystem(device: .mockWith(osVersion: "1")) - XCTAssertEqual(info.versionMajor, "1") - - info = RUMOperatingSystem(device: .mockWith(osVersion: "0.1.2-beta1")) - XCTAssertEqual(info.versionMajor, "0") - - info = RUMOperatingSystem(device: .mockWith(osVersion: "invalid_version")) - XCTAssertEqual(info.versionMajor, "invalid_version") - } -} diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 2eb6013b39..267c7584c4 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -132,7 +132,7 @@ class RUMSessionScopeTests: XCTestCase { // Given let dateProvider = RelativeDateProvider() let ttl: TimeInterval = .mockRandom(min: 2, max: 10) - let viewCache = ViewCache(ttl: ttl) + let viewCache = ViewCache(dateProvider: SystemDateProvider(), ttl: ttl) let scope: RUMSessionScope = .mockWith( parent: parent, diff --git a/DatadogSessionReplay.podspec b/DatadogSessionReplay.podspec index aff5432cfc..815d556472 100644 --- a/DatadogSessionReplay.podspec +++ b/DatadogSessionReplay.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogSessionReplay" - s.version = "2.19.0" + s.version = "2.20.0" s.summary = "Official Datadog Session Replay SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard index 464f532a0e..540c639cba 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard @@ -1,9 +1,9 @@ - + - + @@ -18,12 +18,24 @@ - + - + + + + + + + + + + + + + @@ -46,6 +58,16 @@ + + + + + + + + + + @@ -77,7 +99,7 @@ - + -