diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5bed1f3ac..5a70d47bbb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,8 +12,4 @@ A brief description of implementation details of this PR. - [ ] Add CHANGELOG entry for user facing changes ### Custom CI job configuration (optional) -- [ ] Run unit tests for Core, RUM, Trace, Logs, CR and WVT - [ ] Run unit tests for Session Replay -- [ ] Run integration tests -- [ ] Run smoke tests -- [ ] Run tests for `tools/` diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddf3917437..df5e5f4923 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,86 +1,183 @@ stages: - - info + - pre - lint - test + - ui-test + - smoke-test -ENV info: - stage: info +variables: + MAIN_BRANCH: "master" + DEVELOP_BRANCH: "develop" + +default: tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:sonoma + - specific:true + +# ┌───────────────┐ +# │ Utility jobs: │ +# └───────────────┘ + +# Trigger jobs on 'develop' and 'master' branches +.run:when-develop-or-master: + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH || $CI_COMMIT_BRANCH == $MAIN_BRANCH' + when: always + +# Trigger jobs on SDK code changes, comparing against 'develop' branch +.run:if-sdk-modified: + rules: + - changes: + paths: + - "Datadog*/**/*" + - "IntegrationTests/**/*" + - "TestUtilities/**/*" + - "*" # match any file in the root directory + compare_to: 'develop' # cannot use variable due to: https://gitlab.com/gitlab-org/gitlab/-/issues/369916 + +# Trigger jobs on changes in `tools/*`, comparing against 'develop' branch +.run:if-tools-modified: + rules: + - changes: + paths: + - "tools/**/*" + - "Makefile" + - ".gitlab-ci.yml" + compare_to: 'develop' + +ENV check: + stage: pre script: - - system_profiler SPSoftwareDataType # system info - - xcodebuild -version - - xcode-select -p # default Xcode - - ls /Applications/ | grep Xcode # other Xcodes - - xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore iOS" -showdestinations -quiet # installed iOS destinations - - xcodebuild -workspace "Datadog.xcworkspace" -scheme "DatadogCore tvOS" -showdestinations -quiet # installed tvOS destinations - - xcbeautify --version - - swiftlint --version - - carthage version - - gh --version - - brew -v - - bundler --version - - python3 -V + - make env-check + +# ┌──────────────────────────┐ +# │ SDK changes integration: │ +# └──────────────────────────┘ Lint: stage: lint - tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + rules: + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] script: - - ./tools/lint/run-linter.sh - - ./tools/license/check-license.sh + - make clean repo-setup ENV=ci + - make lint license-check + - make rum-models-verify sr-models-verify -SDK unit tests (iOS): +Unit Tests (iOS): stage: test - tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + rules: + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.0.1" + XCODE: "15.3.0" + OS: "17.4" + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" script: - - make dependencies-gitlab - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogCoreTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogInternalTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogLogsTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogTraceTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogRUMTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore iOS" -only-testing:"DatadogWebViewTrackingTests iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogSessionReplay iOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCrashReporting iOS" test | xcbeautify - -SDK unit tests (tvOS): + - ./tools/runner-setup.sh --xcode "$XCODE" --iOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make test-ios-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +Unit Tests (tvOS): stage: test - tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + rules: + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] + variables: + XCODE: "15.3.0" + OS: "17.4" + PLATFORM: "tvOS Simulator" + DEVICE: "Apple TV" + script: + - ./tools/runner-setup.sh --xcode "$XCODE" --tvOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make test-tvos-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +UI Tests: + stage: ui-test + rules: + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-sdk-modified, rules] variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=tvOS Simulator,name=Apple TV,OS=17.0" + XCODE: "15.3.0" + OS: "17.4" + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + parallel: + matrix: + - TEST_PLAN: + - Default + - RUM + - CrashReporting + - NetworkInstrumentation script: - - make dependencies-gitlab - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogCoreTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogInternalTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogLogsTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogTraceTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCore tvOS" -only-testing:"DatadogRUMTests tvOS" test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "DatadogCrashReporting tvOS" test | xcbeautify - -SDK integration tests (iOS): + - ./tools/runner-setup.sh --xcode "$XCODE" --iOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make ui-test TEST_PLAN="$TEST_PLAN" OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +Tools Tests: stage: test + rules: + - !reference [.run:when-develop-or-master, rules] + - !reference [.run:if-tools-modified, rules] + script: + - make clean repo-setup ENV=ci + - make tools-test + +Smoke Tests (iOS): + stage: smoke-test + tags: + - macos:ventura + - specific:true + variables: + XCODE: "15.2.0" + OS: "17.2" + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + script: + - ./tools/runner-setup.sh --xcode "$XCODE" --iOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make spm-build-ios + - make smoke-test-ios-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +Smoke Tests (tvOS): + stage: smoke-test + tags: + - macos:ventura + - specific:true + variables: + XCODE: "15.2.0" + OS: "17.2" + PLATFORM: "tvOS Simulator" + DEVICE: "Apple TV" + script: + - ./tools/runner-setup.sh --xcode "$XCODE" --tvOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make spm-build-tvos + - make smoke-test-tvos-all OS="$OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +Smoke Tests (visionOS): + stage: smoke-test + tags: + - macos:ventura + - specific:true + variables: + XCODE: "15.2.0" + OS: "1.0" + script: + - ./tools/runner-setup.sh --xcode "$XCODE" --visionOS --os "$OS" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make spm-build-visionos + +Smoke Tests (macOS): + stage: smoke-test tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + - macos:ventura + - specific:true variables: - TEST_WORKSPACE: "IntegrationTests/IntegrationTests.xcworkspace" - TEST_DESTINATION: "platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.0.1" + XCODE: "15.2.0" script: - - make dependencies-gitlab - - make prepare-integration-tests - # Before running crash reporting tests, disable Apple Crash Reporter so it doesn't capture the crash causing tests hang on " quit unexpectedly" prompt: - - launchctl unload -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist - - ./tools/config/generate-http-server-mock-config.sh - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "IntegrationScenarios" -testPlan DatadogIntegrationTests test | xcbeautify - - xcodebuild -workspace "$TEST_WORKSPACE" -destination "$TEST_DESTINATION" -scheme "IntegrationScenarios" -testPlan DatadogCrashReportingIntegrationTests test | xcbeautify + - ./tools/runner-setup.sh --xcode "$XCODE" # temporary, waiting for AMI + - make clean repo-setup ENV=ci + - make spm-build-macos diff --git a/CHANGELOG.md b/CHANGELOG.md index 283ebe244d..12b8c5baa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Unreleased +# 2.14.0 / 04-07-2024 + +- [IMPROVEMENT] Use `#fileID` over `#filePath` as the default argument in errors. See [#1938][] +- [FEATURE] Add support for Watchdog Terminations tracking in RUM. See [#1917][] [#1911][] [#1912][] [#1889][] +- [IMPROVEMENT] Tabbar Icon Default Tint Color in Session Replay. See [#1906][] +- [IMPROVEMENT] Improve Nav Bar Support in Session Replay. See [#1916][] +- [IMPROVEMENT] Record Activity Indicator in Session Replay. See [#1934][] +- [IMPROVEMENT] Allow disabling app hang monitoring in ObjC API. See [#1908][] +- [IMPROVEMENT] Update RUM and Telemetry models with KMP source. See [#1925][] + +# 2.11.1 / 01-07-2024 + +- [FIX] Fix compilation issues on Xcode 16 beta. See [#1898][] + # 2.13.0 / 13-06-2024 - [IMPROVEMENT] Bump `IPHONEOS_DEPLOYMENT_TARGET` and `TVOS_DEPLOYMENT_TARGET` from 11 to 12. See [#1891][] @@ -683,7 +697,17 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1828]: https://github.com/DataDog/dd-sdk-ios/pull/1828 [#1835]: https://github.com/DataDog/dd-sdk-ios/pull/1835 [#1886]: https://github.com/DataDog/dd-sdk-ios/pull/1886 +[#1889]: https://github.com/DataDog/dd-sdk-ios/pull/1889 [#1898]: https://github.com/DataDog/dd-sdk-ios/pull/1898 +[#1906]: https://github.com/DataDog/dd-sdk-ios/pull/1906 +[#1908]: https://github.com/DataDog/dd-sdk-ios/pull/1908 +[#1911]: https://github.com/DataDog/dd-sdk-ios/pull/1911 +[#1912]: https://github.com/DataDog/dd-sdk-ios/pull/1912 +[#1916]: https://github.com/DataDog/dd-sdk-ios/pull/1916 +[#1917]: https://github.com/DataDog/dd-sdk-ios/pull/1917 +[#1925]: https://github.com/DataDog/dd-sdk-ios/pull/1925 +[#1934]: https://github.com/DataDog/dd-sdk-ios/pull/1934 +[#1938]: https://github.com/DataDog/dd-sdk-ios/pull/1938 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu diff --git a/Cartfile b/Cartfile index 31328b20d2..e3155c952c 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ github "microsoft/plcrashreporter" ~> 1.11.2 -binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" ~> 1.6.0 +binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" == 1.6.0 diff --git a/Datadog.xcworkspace/contents.xcworkspacedata b/Datadog.xcworkspace/contents.xcworkspacedata index 6d86ab3776..d52e6b154b 100644 --- a/Datadog.xcworkspace/contents.xcworkspacedata +++ b/Datadog.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 6c60348a5f..e5c9f41ff7 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -13,6 +13,10 @@ 1434A4642B7F73170072E3BB /* OpenTelemetryApi.xcframework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; + 3C08F9D02C2D652D002B0FF2 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */; }; + 3C08F9D12C2D652D002B0FF2 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */; }; + 3C0CB3452C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */; }; + 3C0CB3462C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */; }; 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DD62A543B3B00446CF9 /* Event.swift */; }; 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */; }; @@ -38,7 +42,11 @@ 3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; 3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */; }; 3C33E4072BEE35A8003B2988 /* RUMContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */; }; + 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */; }; + 3C3EF2B12C1AEBAB009E9E57 /* LaunchReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */; }; 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; + 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; + 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; @@ -67,6 +75,8 @@ 3C9B27252B9F174700569C07 /* SpanID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9B27242B9F174700569C07 /* SpanID.swift */; }; 3C9B27262B9F174700569C07 /* SpanID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C9B27242B9F174700569C07 /* SpanID.swift */; }; 3C9C6BB429F7C0C000581C43 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + 3CA00B072C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */; }; + 3CA00B082C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */; }; 3CA8525F2BF2073800B52CBA /* TraceContextInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */; }; 3CA852602BF2073800B52CBA /* TraceContextInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */; }; 3CA852642BF2148200B52CBA /* TraceContextInjection+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */; }; @@ -95,8 +105,22 @@ 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3CEC57732C16FD0B0042B5F2 /* WatchdogTerminationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */; }; + 3CEC57742C16FD0C0042B5F2 /* WatchdogTerminationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */; }; + 3CEC57772C16FDD70042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */; }; + 3CEC57782C16FDD80042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */; }; 3CF673362B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; 3CF673372B4807490016CE17 /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF673352B4807490016CE17 /* OTelSpanTests.swift */; }; + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */; }; + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */; }; + 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */; }; + 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */; }; + 3CFF4F942C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */; }; + 3CFF4F952C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */; }; + 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */; }; + 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */; }; + 3CFF4FA42C0E0FE8006F191D /* WatchdogTerminationCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */; }; + 3CFF4FA52C0E0FE9006F191D /* WatchdogTerminationCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */; }; 3CFF5D492B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; 3CFF5D4A2B555F4F00FC483A /* OTelTracerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */; }; 49274906288048B500ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; @@ -531,8 +555,6 @@ 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */; }; 61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BBD19624ED50040023E65F /* DatadogConfigurationTests.swift */; }; 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */; }; - 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */; }; - 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638424361E9200C4D4E6 /* Globals.swift */; }; 61C4534A2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */; }; 61C4534B2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */; }; 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89524509BF600DA608C /* TracerTests.swift */; }; @@ -637,6 +659,10 @@ 61FDBA15269722B4001D9D43 /* CrashReportMinifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA14269722B4001D9D43 /* CrashReportMinifierTests.swift */; }; 61FDBA1726974CA9001D9D43 /* DDCrashReportBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA1626974CA9001D9D43 /* DDCrashReportBuilderTests.swift */; }; 61FF282824B8A31E000B3D9B /* RUMEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282724B8A31E000B3D9B /* RUMEventMatcher.swift */; }; + 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */; }; + 969B3B232C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */; }; + 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */; }; + 96E414162C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E414152C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift */; }; 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E55407B25812D1C00F6E3AD /* RUM+objc.swift */; }; 9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */; }; 9E5B6D2E270C84B4002499B8 /* RUMMonitorE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5B6D2D270C84B4002499B8 /* RUMMonitorE2ETests.swift */; }; @@ -1106,6 +1132,8 @@ D27D81C62A5D415200281CC2 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; D27D81C72A5D415200281CC2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; D27D81C82A5D41F400281CC2 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D284C7402C2059F3005142CC /* ObjcExceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */; }; + D284C7412C2059F3005142CC /* ObjcExceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */; }; D286626E2A43487500852CE3 /* Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D286626D2A43487500852CE3 /* Datadog.swift */; }; D286626F2A43487500852CE3 /* Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D286626D2A43487500852CE3 /* Datadog.swift */; }; D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */; }; @@ -1396,7 +1424,6 @@ D2CB6E3627C50EAE00A62B57 /* ObjcAppLaunchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */; }; D2CB6E3C27C50EAE00A62B57 /* Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD702589FAFD007E8BB7 /* Retrying.swift */; }; D2CB6E4327C50EAE00A62B57 /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; - D2CB6E4D27C50EAE00A62B57 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638424361E9200C4D4E6 /* Globals.swift */; }; D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */; }; D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E792E2577B0F900DFCC17 /* Reader.swift */; }; D2CB6E6927C50EAE00A62B57 /* KronosDNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */; }; @@ -1430,7 +1457,6 @@ D2CB6EF427C520D400A62B57 /* FileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C292423990D00786299 /* FileWriterTests.swift */; }; D2CB6EFE27C520D400A62B57 /* RUMMonitorConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B954124BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift */; }; D2CB6F0027C520D400A62B57 /* RUMSessionMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */; }; - D2CB6F0127C520D400A62B57 /* DatadogPrivateMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */; }; D2CB6F0427C520D400A62B57 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; D2CB6F0927C520D400A62B57 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; D2CB6F0C27C520D400A62B57 /* KronosNTPPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */; }; @@ -2063,6 +2089,8 @@ /* Begin PBXFileReference section */ 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOTelTracingViewController.swift; sourceTree = ""; }; + 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationReporter.swift; sourceTree = ""; }; 3C0D5DD62A543B3B00446CF9 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGenerator.swift; sourceTree = ""; }; 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventGeneratorTests.swift; sourceTree = ""; }; @@ -2075,6 +2103,8 @@ 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLink.swift; sourceTree = ""; }; 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanLinkTests.swift; sourceTree = ""; }; 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.swift; sourceTree = ""; }; + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReport.swift; sourceTree = ""; }; + 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitorTests.swift; sourceTree = ""; }; 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+Datadog.swift"; sourceTree = ""; }; 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+DatadogTests.swift"; sourceTree = ""; }; 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; @@ -2087,6 +2117,7 @@ 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTracking.swift; sourceTree = ""; }; 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizerMock.swift; sourceTree = ""; }; 3C9B27242B9F174700569C07 /* SpanID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanID.swift; sourceTree = ""; }; + 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationsMonitoringTests.swift; sourceTree = ""; }; 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceContextInjection.swift; sourceTree = ""; }; 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceContextInjection+objc.swift"; sourceTree = ""; }; 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpan.swift; sourceTree = ""; }; @@ -2101,7 +2132,14 @@ 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDTests.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogWebViewTrackingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMocks.swift; sourceTree = ""; }; + 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppStateManagerTests.swift; sourceTree = ""; }; 3CF673352B4807490016CE17 /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.swift; sourceTree = ""; }; + 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppState.swift; sourceTree = ""; }; + 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationAppStateManager.swift; sourceTree = ""; }; + 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationChecker.swift; sourceTree = ""; }; + 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitor.swift; sourceTree = ""; }; + 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationCheckerTests.swift; sourceTree = ""; }; 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelTracerProvider.swift; sourceTree = ""; }; 49274903288048AA00ECD49B /* InternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalProxyTests.swift; sourceTree = ""; }; 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMInternalProxyTests.swift; sourceTree = ""; }; @@ -2537,8 +2575,6 @@ 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionScopeTests.swift; sourceTree = ""; }; 61C2C21124C5951400C0321C /* RUMViewScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewScope.swift; sourceTree = ""; }; 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionHandlerTests.swift; sourceTree = ""; }; - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogPrivateMocks.swift; sourceTree = ""; }; - 61C3638424361E9200C4D4E6 /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMock.swift; sourceTree = ""; }; 61C3E63624BF191F008053F2 /* RUMScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScope.swift; sourceTree = ""; }; 61C3E63824BF19B4008053F2 /* RUMContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMContext.swift; sourceTree = ""; }; @@ -2652,6 +2688,10 @@ 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventFileOutputTests.swift; sourceTree = ""; }; 61FF416125EE5FF400CE35EC /* CrashLogReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashLogReceiverTests.swift; sourceTree = ""; }; 61FF9A4425AC5DEA001058CC /* ViewIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewIdentifier.swift; sourceTree = ""; }; + 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityIndicatorRecorder.swift; sourceTree = ""; }; + 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityIndicatorRecorderTests.swift; sourceTree = ""; }; + 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIProgressViewRecorder.swift; sourceTree = ""; }; + 96E414152C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIProgressViewRecorderTests.swift; sourceTree = ""; }; 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Kronos.xcframework; path = ../Carthage/Build/Kronos.xcframework; sourceTree = ""; }; 9E26E6B824C87693000B3270 /* RUMDataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMDataModels.swift; sourceTree = ""; }; 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoSamplerTests.swift; sourceTree = ""; }; @@ -2879,6 +2919,7 @@ D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.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 = ""; }; D286626D2A43487500852CE3 /* Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = ""; }; D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = ""; }; D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandlerTests.swift; sourceTree = ""; }; @@ -3360,6 +3401,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */ = { + isa = PBXGroup; + children = ( + 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */, + 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */, + 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */, + 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */, + 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */, + ); + path = WatchdogTerminations; + sourceTree = ""; + }; 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( @@ -3413,6 +3466,17 @@ path = ../DatadogWebViewTracking/Tests; sourceTree = ""; }; + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */ = { + isa = PBXGroup; + children = ( + 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */, + 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */, + 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */, + 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */, + ); + path = WatchdogTerminations; + sourceTree = ""; + }; 61020C272757AD63005EEAEA /* BackgroundEvents */ = { isa = PBXGroup; children = ( @@ -3549,6 +3613,7 @@ 61054E272A6EE10A00AAA894 /* NodeRecorders */ = { isa = PBXGroup; children = ( + 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */, 61054E282A6EE10A00AAA894 /* UIDatePickerRecorder.swift */, 61054E292A6EE10A00AAA894 /* UITextViewRecorder.swift */, 61054E2A2A6EE10A00AAA894 /* UIImageViewRecorder.swift */, @@ -3559,6 +3624,7 @@ 61054E2E2A6EE10A00AAA894 /* NodeRecorder.swift */, 61054E2F2A6EE10A00AAA894 /* UISliderRecorder.swift */, 61054E302A6EE10A00AAA894 /* UIPickerViewRecorder.swift */, + 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */, 61054E312A6EE10A00AAA894 /* UIStepperRecorder.swift */, 61054E322A6EE10A00AAA894 /* UILabelRecorder.swift */, 61054E332A6EE10A00AAA894 /* UISwitchRecorder.swift */, @@ -3823,9 +3889,11 @@ 61054F692A6EE1BA00AAA894 /* NodeRecorders */ = { isa = PBXGroup; children = ( + 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */, 61054F6A2A6EE1BA00AAA894 /* UILabelRecorderTests.swift */, 61054F6B2A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift */, 61054F6C2A6EE1BA00AAA894 /* UITabBarRecorderTests.swift */, + 96E414152C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift */, 61054F6D2A6EE1BA00AAA894 /* UISliderRecorderTests.swift */, 61054F6E2A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift */, 61054F6F2A6EE1BA00AAA894 /* UISegmentRecorderTests.swift */, @@ -4100,7 +4168,6 @@ 61133BB72423979B00786299 /* Utils */ = { isa = PBXGroup; children = ( - 61C3638424361E9200C4D4E6 /* Globals.swift */, 6139CD702589FAFD007E8BB7 /* Retrying.swift */, 61DA8CAE28620C760074A606 /* Cryptography.swift */, ); @@ -4243,7 +4310,6 @@ D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */, D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */, 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */, - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, D20605BB28757BFB0047275C /* KronosClockMock.swift */, 61F1A61B2498AD2C00075390 /* SystemFrameworks */, ); @@ -4714,6 +4780,7 @@ 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */, 6167E6F52B81E94C00C3CA2D /* DDThread.swift */, 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */, + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */, ); path = CrashReporting; sourceTree = ""; @@ -4757,6 +4824,7 @@ 6157FA5C252767B3009A8A3B /* Resources */, 9E06058F26EF904200F5F935 /* LongTasks */, 6167E6D12B7F8B1300C3CA2D /* AppHangs */, + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */, ); path = Instrumentation; sourceTree = ""; @@ -5334,6 +5402,7 @@ children = ( 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */, 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */, + 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */, D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */, 61DCC84C2C05D4E500CB59E5 /* SDKMetrics */, ); @@ -5419,6 +5488,7 @@ 6141014C251A577D00E3C2D9 /* Actions */, 613F23EF252B1287006CD2D7 /* Resources */, 6167E6D82B80047900C3CA2D /* AppHangs */, + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */, 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */, ); path = Instrumentation; @@ -5719,6 +5789,7 @@ D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */, D23039BD298D5235001A1FA3 /* DatadogFeature.swift */, D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */, + 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */, D23039AD298D5234001A1FA3 /* DD.swift */, D23039D6298D5235001A1FA3 /* Extensions */, D23039BF298D5235001A1FA3 /* MessageBus */, @@ -6202,6 +6273,7 @@ children = ( 613C6B912768FF3100870CBF /* SamplerTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */, ); path = Utils; sourceTree = ""; @@ -7940,7 +8012,6 @@ 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, D2FB125D292FBB56005B13F8 /* Datadog+Internal.swift in Sources */, D2A7840F29A53B2F003B03BB /* Directory.swift in Sources */, - 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */, D20605CB2875A83F0047275C /* ContextValueReader.swift in Sources */, 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */, D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */, @@ -8024,7 +8095,6 @@ 6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */, 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */, 6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */, - 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, 6147989C2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */, D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */, @@ -8034,6 +8104,7 @@ 61A2CC212A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */, D2A434AE2A8E426C0028E329 /* DDSessionReplayTests.swift in Sources */, 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */, + 3CA00B072C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift in Sources */, 6167E70E2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift in Sources */, E143CCAF27D236F600F4018A /* CITestIntegrationTests.swift in Sources */, 6128F57E2BA8A3A000D35B08 /* DataStore+TLVTests.swift in Sources */, @@ -8183,6 +8254,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */, 61054EA22A6EE10B00AAA894 /* Scheduler.swift in Sources */, A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */, A7B932FB2B1F6A0A00AE6477 /* EnrichedRecord.swift in Sources */, @@ -8218,6 +8290,7 @@ 61054E9C2A6EE10B00AAA894 /* UIImage+Scaling.swift in Sources */, 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */, 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */, + 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */, 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */, @@ -8265,6 +8338,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 969B3B232C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift in Sources */, 61054FD42A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift in Sources */, 61054FB52A6EE1BA00AAA894 /* UISliderRecorderTests.swift in Sources */, 61054FB22A6EE1BA00AAA894 /* UILabelRecorderTests.swift in Sources */, @@ -8274,6 +8348,7 @@ 61054FC02A6EE1BA00AAA894 /* UITextViewRecorderTests.swift in Sources */, 61054F9A2A6EE1BA00AAA894 /* CFType+SafetyTests.swift in Sources */, 61054F9E2A6EE1BA00AAA894 /* SessionReplayTests.swift in Sources */, + 96E414162C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift in Sources */, 61054FB32A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift in Sources */, 61054FCD2A6EE1BA00AAA894 /* SnapshotProducerMocks.swift in Sources */, 61054FC32A6EE1BA00AAA894 /* PrivacyLevelTests.swift in Sources */, @@ -8577,6 +8652,7 @@ D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */, D2D3199A29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56A2BA2237300D35B08 /* DataStore.swift in Sources */, + 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, 6167E7002B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */, D2EBEE2729BA160F00B15732 /* B3HTTPHeadersWriter.swift in Sources */, D23039E2298D5236001A1FA3 /* UserInfo.swift in Sources */, @@ -8590,6 +8666,7 @@ D2160CA029C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D295A16529F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */, + 3C08F9D02C2D652D002B0FF2 /* Storage.swift in Sources */, D23039E5298D5236001A1FA3 /* DateProvider.swift in Sources */, D23039E0298D5235001A1FA3 /* DatadogCoreProtocol.swift in Sources */, D23039FD298D5236001A1FA3 /* DataCompression.swift in Sources */, @@ -8621,6 +8698,7 @@ D253EE972B988CA90010B589 /* ViewCache.swift in Sources */, D23F8E5A29DDCD28001CFAE8 /* RUMResourceScope.swift in Sources */, D23F8E5C29DDCD28001CFAE8 /* RUMApplicationScope.swift in Sources */, + 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, D23F8E5D29DDCD28001CFAE8 /* SwiftUIViewModifier.swift in Sources */, D23F8E5E29DDCD28001CFAE8 /* VitalInfo.swift in Sources */, D23F8E5F29DDCD28001CFAE8 /* UIApplicationSwizzler.swift in Sources */, @@ -8630,6 +8708,7 @@ D23F8E6329DDCD28001CFAE8 /* RUMDataModels.swift in Sources */, 61C713AB2A3B790B00FA735A /* Monitor.swift in Sources */, D23F8E6429DDCD28001CFAE8 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, D23F8E6529DDCD28001CFAE8 /* RUMFeature.swift in Sources */, D23F8E6629DDCD28001CFAE8 /* RUMDebugging.swift in Sources */, D23F8E6729DDCD28001CFAE8 /* RUMUUID.swift in Sources */, @@ -8643,6 +8722,7 @@ 49D8C0B82AC5D2160075E427 /* RUM+Internal.swift in Sources */, D23F8E6E29DDCD28001CFAE8 /* RUMViewsHandler.swift in Sources */, 61C713BA2A3C935C00FA735A /* RUM.swift in Sources */, + 3C0CB3462C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */, D23F8E6F29DDCD28001CFAE8 /* RequestBuilder.swift in Sources */, D224430529E9588500274EC7 /* TelemetryReceiver.swift in Sources */, D23F8E7029DDCD28001CFAE8 /* URLSessionRUMResourcesHandler.swift in Sources */, @@ -8675,6 +8755,7 @@ D23F8E8329DDCD28001CFAE8 /* RUMUser.swift in Sources */, D23F8E8429DDCD28001CFAE8 /* UIKitRUMUserActionsPredicate.swift in Sources */, D23F8E8529DDCD28001CFAE8 /* SwiftUIExtensions.swift in Sources */, + 3CFF4F952C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D23F8E8629DDCD28001CFAE8 /* RUMDataModelsMapping.swift in Sources */, D23F8E8729DDCD28001CFAE8 /* RUMInstrumentation.swift in Sources */, D23F8E8829DDCD28001CFAE8 /* VitalCPUReader.swift in Sources */, @@ -8684,6 +8765,7 @@ D23F8E8C29DDCD28001CFAE8 /* RUMBaggageKeys.swift in Sources */, 6174D6212C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, D23F8E8E29DDCD28001CFAE8 /* UIKitRUMUserActionsHandler.swift in Sources */, D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */, 61DCC84F2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, @@ -8706,16 +8788,19 @@ 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 */, D23F8EAD29DDCD38001CFAE8 /* RUMEventBuilderTests.swift in Sources */, 61CE2E602BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, + 3CEC57782C16FDD80042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */, D23F8EAE29DDCD38001CFAE8 /* DDTAssertValidRUMUUID.swift in Sources */, D23F8EAF29DDCD38001CFAE8 /* RUMScopeTests.swift in Sources */, D23F8EB029DDCD38001CFAE8 /* SessionReplayDependencyTests.swift in Sources */, 61C713B72A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D23F8EB129DDCD38001CFAE8 /* RUMViewScopeTests.swift in Sources */, D224431029E977A100274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */, 6174D61B2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -8732,6 +8817,7 @@ D23F8EBE29DDCD38001CFAE8 /* WebViewEventReceiverTests.swift in Sources */, D23F8EBF29DDCD38001CFAE8 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, D23F8EC029DDCD38001CFAE8 /* RUMEventSanitizerTests.swift in Sources */, + 3CEC57742C16FD0C0042B5F2 /* WatchdogTerminationMocks.swift in Sources */, D253EE9C2B98B37C0010B589 /* ViewCacheTests.swift in Sources */, 6176C1732ABDBA2E00131A70 /* MonitorTests.swift in Sources */, D23F8EC129DDCD38001CFAE8 /* RUMEventsMapperTests.swift in Sources */, @@ -8941,6 +9027,7 @@ D253EE962B988CA90010B589 /* ViewCache.swift in Sources */, D29A9F8429DD85BB005C54A4 /* RUMResourceScope.swift in Sources */, D29A9F7329DD85BB005C54A4 /* RUMApplicationScope.swift in Sources */, + 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, D29A9F6A29DD85BB005C54A4 /* SwiftUIViewModifier.swift in Sources */, D29A9F6429DD85BB005C54A4 /* VitalInfo.swift in Sources */, D29A9F6D29DD85BB005C54A4 /* UIApplicationSwizzler.swift in Sources */, @@ -8950,6 +9037,7 @@ D29A9F7B29DD85BB005C54A4 /* RUMDataModels.swift in Sources */, 61C713AA2A3B790B00FA735A /* Monitor.swift in Sources */, D29A9F8529DD85BB005C54A4 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, D29A9F7429DD85BB005C54A4 /* RUMFeature.swift in Sources */, D29A9F7729DD85BB005C54A4 /* RUMDebugging.swift in Sources */, D29A9F6E29DD85BB005C54A4 /* RUMUUID.swift in Sources */, @@ -8963,6 +9051,7 @@ 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */, D29A9F7629DD85BB005C54A4 /* RUMViewsHandler.swift in Sources */, 61C713B92A3C935C00FA735A /* RUM.swift in Sources */, + 3C0CB3452C19A1ED003B0E9B /* WatchdogTerminationReporter.swift in Sources */, D29A9F7929DD85BB005C54A4 /* RequestBuilder.swift in Sources */, D224430429E9588100274EC7 /* TelemetryReceiver.swift in Sources */, D29A9F5729DD85BB005C54A4 /* URLSessionRUMResourcesHandler.swift in Sources */, @@ -8995,6 +9084,7 @@ D29A9F6629DD85BB005C54A4 /* RUMUser.swift in Sources */, D29A9F8229DD85BB005C54A4 /* UIKitRUMUserActionsPredicate.swift in Sources */, D29A9F8E29DD8665005C54A4 /* SwiftUIExtensions.swift in Sources */, + 3CFF4F942C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D29A9F7829DD85BB005C54A4 /* RUMDataModelsMapping.swift in Sources */, D29A9F6F29DD85BB005C54A4 /* RUMInstrumentation.swift in Sources */, D29A9F7A29DD85BB005C54A4 /* VitalCPUReader.swift in Sources */, @@ -9004,6 +9094,7 @@ D29A9F8329DD85BB005C54A4 /* RUMBaggageKeys.swift in Sources */, 6174D6202C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, D29A9F6929DD85BB005C54A4 /* UIKitRUMUserActionsHandler.swift in Sources */, D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */, 61DCC84E2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, @@ -9026,16 +9117,19 @@ 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 */, D29A9FBE29DDB483005C54A4 /* RUMEventBuilderTests.swift in Sources */, 61CE2E5F2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */, + 3CEC57772C16FDD70042B5F2 /* WatchdogTerminationAppStateManagerTests.swift in Sources */, D29A9FCC29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift in Sources */, D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */, D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */, 61C713B62A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */, D224430F29E9779F00274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -9052,6 +9146,7 @@ D29A9FA429DDB483005C54A4 /* WebViewEventReceiverTests.swift in Sources */, D29A9F9A29DDB483005C54A4 /* URLSessionRUMResourcesHandlerTests.swift in Sources */, D29A9FA229DDB483005C54A4 /* RUMEventSanitizerTests.swift in Sources */, + 3CEC57732C16FD0B0042B5F2 /* WatchdogTerminationMocks.swift in Sources */, D253EE9B2B98B37B0010B589 /* ViewCacheTests.swift in Sources */, 6176C1722ABDBA2E00131A70 /* MonitorTests.swift in Sources */, D29A9FB929DDB483005C54A4 /* RUMEventsMapperTests.swift in Sources */, @@ -9189,7 +9284,6 @@ D29294E1291D5ED500F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, D20605A7287476230047275C /* ServerOffsetPublisher.swift in Sources */, D21C26C628A3B49C005DD405 /* FeatureStorage.swift in Sources */, - D2CB6E4D27C50EAE00A62B57 /* Globals.swift in Sources */, D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */, D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */, D2CB6E6927C50EAE00A62B57 /* KronosDNSResolver.swift in Sources */, @@ -9261,7 +9355,6 @@ D2CB6EFE27C520D400A62B57 /* RUMMonitorConfigurationTests.swift in Sources */, D2CB6F0027C520D400A62B57 /* RUMSessionMatcher.swift in Sources */, A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */, - D2CB6F0127C520D400A62B57 /* DatadogPrivateMocks.swift in Sources */, 6167E6DE2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, D24C9C7229A7D57A002057CF /* DirectoriesMock.swift in Sources */, @@ -9269,6 +9362,7 @@ D2CB6F0427C520D400A62B57 /* DDTracerTests.swift in Sources */, D24C9C6129A7CB0C002057CF /* DatadogLogsFeatureTests.swift in Sources */, D29A9FCF29DDC4BC005C54A4 /* RUMFeatureMocks.swift in Sources */, + 3CA00B082C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift in Sources */, D22743DE29DEB8B5001A7EF9 /* VitalInfoSamplerTests.swift in Sources */, D2CB6F0927C520D400A62B57 /* RUMDataModels+objcTests.swift in Sources */, D234613128B7713000055D4C /* FeatureContextTests.swift in Sources */, @@ -9525,6 +9619,7 @@ D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */, D2D3199B29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */, 6128F56B2BA2237300D35B08 /* DataStore.swift in Sources */, + 3C3EF2B12C1AEBAB009E9E57 /* LaunchReport.swift in Sources */, 6167E7012B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */, D2EBEE3529BA161100B15732 /* B3HTTPHeadersWriter.swift in Sources */, D2DA2376298D57AA00C6C7E6 /* UserInfo.swift in Sources */, @@ -9538,6 +9633,7 @@ D2160CA129C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */, D22F06D829DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */, D295A16629F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */, + 3C08F9D12C2D652D002B0FF2 /* Storage.swift in Sources */, D2DA237B298D57AA00C6C7E6 /* DateProvider.swift in Sources */, D2DA237C298D57AA00C6C7E6 /* DatadogCoreProtocol.swift in Sources */, D2DA237D298D57AA00C6C7E6 /* DataCompression.swift in Sources */, @@ -9577,6 +9673,7 @@ D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D2DA23A1298D58F400C6C7E6 /* ReadWriteLockTests.swift in Sources */, D2160CD829C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */, + D284C7402C2059F3005142CC /* ObjcExceptionTests.swift in Sources */, D2C5D5282B83FD5300B63F36 /* WebViewMessageTests.swift in Sources */, D20731CD29A52E8700ECBF94 /* SamplerTests.swift in Sources */, D2DA23A6298D58F400C6C7E6 /* AnyCoderTests.swift in Sources */, @@ -9626,6 +9723,7 @@ D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, D2DA23B5298D59DC00C6C7E6 /* ReadWriteLockTests.swift in Sources */, D2160CD929C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */, + D284C7412C2059F3005142CC /* ObjcExceptionTests.swift in Sources */, D2C5D5292B83FD5400B63F36 /* WebViewMessageTests.swift in Sources */, D20731CE29A52E8700ECBF94 /* SamplerTests.swift in Sources */, D2160CEA29C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index d2e0f0741f..8eea0f6703 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -197,64 +197,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Identifier = "OTelSpanTests/testSetActive_givenParentSpan()"> diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme index 5bd2dcf496..a3a85bc4c8 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme @@ -39,7 +39,7 @@ + Identifier = "OTelSpanTests/testSetActive_givenParentSpan()"> diff --git a/Datadog/Example/Debugging/DebugRUMSessionViewController.swift b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift index 5a6e81ff9a..73cb64e9e4 100644 --- a/Datadog/Example/Debugging/DebugRUMSessionViewController.swift +++ b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift @@ -33,7 +33,10 @@ private class DebugRUMSessionViewModel: ObservableObject { var id: UUID = UUID() } - @Published var sessionItems: [SessionItem] = [] + @Published var sessionItems: [SessionItem] = [] { + didSet { updateSessionID() } + } + @Published var sessionID: String = "" @Published var viewKey: String = "" @Published var actionName: String = "" @@ -46,12 +49,18 @@ private class DebugRUMSessionViewModel: ObservableObject { var urlSessions: [URLSession] = [] + init() { + updateSessionID() + } + func startView() { guard !viewKey.isEmpty else { return } let key = viewKey + RUMMonitor.shared().startView(key: key) + sessionItems.append( SessionItem( label: key, @@ -67,7 +76,6 @@ private class DebugRUMSessionViewModel: ObservableObject { ) ) - RUMMonitor.shared().startView(key: key) self.viewKey = "" } @@ -76,11 +84,11 @@ private class DebugRUMSessionViewModel: ObservableObject { return } + RUMMonitor.shared().addAction(type: .custom, name: actionName) sessionItems.append( SessionItem(label: actionName, type: .action, isPending: false, stopAction: nil) ) - RUMMonitor.shared().addAction(type: .custom, name: actionName) self.actionName = "" } @@ -89,11 +97,11 @@ private class DebugRUMSessionViewModel: ObservableObject { return } + RUMMonitor.shared().addError(message: errorMessage) sessionItems.append( SessionItem(label: errorMessage, type: .error, isPending: false, stopAction: nil) ) - RUMMonitor.shared().addError(message: errorMessage) self.errorMessage = "" } @@ -103,6 +111,7 @@ private class DebugRUMSessionViewModel: ObservableObject { } let key = self.resourceKey + RUMMonitor.shared().startResource(resourceKey: key, url: mockURL()) sessionItems.append( SessionItem( label: key, @@ -118,7 +127,6 @@ private class DebugRUMSessionViewModel: ObservableObject { ) ) - RUMMonitor.shared().startResource(resourceKey: key, url: mockURL()) self.resourceKey = "" } @@ -161,6 +169,11 @@ private class DebugRUMSessionViewModel: ObservableObject { urlSessions.append(session) // keep session } + func stopSession() { + RUMMonitor.shared().stopSession() + sessionItems = [] + } + // MARK: - Private private func modifySessionItem(type: SessionItemType, label: String, change: (inout SessionItem) -> Void) { @@ -176,6 +189,14 @@ private class DebugRUMSessionViewModel: ObservableObject { private func mockURL() -> URL { return URL(string: "https://foo.com/\(UUID().uuidString)")! } + + private func updateSessionID() { + RUMMonitor.shared().currentSessionID { [weak self] id in + DispatchQueue.main.async { + self?.sessionID = id ?? "-" + } + } + } } @available(iOS 13.0, *) @@ -215,6 +236,10 @@ internal struct DebugRUMSessionView: View { ) Button("START") { viewModel.startResource() } } + HStack { + Button("STOP SESSION") { viewModel.stopSession() } + Spacer() + } Divider() } Group { @@ -248,9 +273,12 @@ internal struct DebugRUMSessionView: View { Divider() } Group { - Text("Current RUM Session:") + Text("Current RUM Session") .frame(maxWidth: .infinity, alignment: .leading) .font(.caption.weight(.bold)) + Text("UUID: \(viewModel.sessionID)") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.ultraLight)) List(viewModel.sessionItems) { sessionItem in SessionItemView(item: sessionItem) .listRowInsets(EdgeInsets()) @@ -277,20 +305,20 @@ private struct FormItemView: View { Text(title) .bold() .font(.system(size: 10)) - .padding(8) + .padding(4) .background(accent) .foregroundColor(Color.white) - .cornerRadius(8) + .cornerRadius(4) TextField(placeholder, text: $value) .font(.system(size: 12)) - .padding(8) + .padding(4) .background(Color(UIColor.secondarySystemFill)) - .cornerRadius(8) + .cornerRadius(4) } - .padding(8) + .padding(4) .background(Color(UIColor.systemFill)) .foregroundColor(Color.secondary) - .cornerRadius(8) + .cornerRadius(4) } } @@ -304,20 +332,20 @@ private struct SessionItemView: View { Text(label(for: item.type)) .bold() .font(.system(size: 10)) - .padding(8) + .padding(4) .background(color(for: item.type)) .foregroundColor(Color.white) - .cornerRadius(8) + .cornerRadius(4) Text(item.label) .bold() .font(.system(size: 14)) Spacer() } - .padding(8) + .padding(4) .frame(maxWidth: .infinity) .background(Color(UIColor.systemFill)) .foregroundColor(Color.secondary) - .cornerRadius(8) + .cornerRadius(4) if item.isPending { Button("STOP") { item.stopAction?() } diff --git a/Datadog/Example/Debugging/Helpers/SwiftUI.swift b/Datadog/Example/Debugging/Helpers/SwiftUI.swift index 8659a9e38c..e06c1c384c 100644 --- a/Datadog/Example/Debugging/Helpers/SwiftUI.swift +++ b/Datadog/Example/Debugging/Helpers/SwiftUI.swift @@ -34,10 +34,10 @@ extension Color { internal struct DatadogButtonStyle: ButtonStyle { func makeBody(configuration: DatadogButtonStyle.Configuration) -> some View { return configuration.label - .font(.system(size: 14, weight: .medium)) - .padding(10) + .font(.system(size: 12, weight: .medium)) + .padding(6) .background(Color.datadogPurple) .foregroundColor(.white) - .cornerRadius(8) + .cornerRadius(6) } } diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift index 6113abc415..6046fd33a1 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -77,8 +77,10 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { resourceAttributesProvider: { req, resp, data, err in print("⭐️ [Attributes Provider] data: \(String(describing: data))") return [:] - }), + } + ), trackBackgroundEvents: true, + trackWatchdogTerminations: true, customEndpoint: Environment.readCustomRUMURL(), telemetrySampleRate: 100 ) diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 307ec2fe97..80f8bcdf81 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -101,7 +101,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { // MARK: - Reporting Session Attributes - func testReportingSessionID() throws { + func testReportingSessionInformation() throws { var currentSessionID: String? RUM.enable(with: rumConfig, in: core) @@ -118,6 +118,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { let metric = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()) let expectedSessionID = try XCTUnwrap(currentSessionID) XCTAssertEqual(metric.session?.id, expectedSessionID.lowercased()) + XCTAssertEqual(metric.attributes?.hasBackgroundEventsTrackingEnabled, rumConfig.trackBackgroundEvents) } func testTrackingSessionDuration() throws { @@ -180,6 +181,8 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { XCTAssertEqual(metricAttributes.viewsCount.total, 10) XCTAssertEqual(metricAttributes.viewsCount.applicationLaunch, 1) XCTAssertEqual(metricAttributes.viewsCount.background, 3) + XCTAssertEqual(metricAttributes.viewsCount.byInstrumentation, ["manual": 6]) + XCTAssertEqual(metricAttributes.viewsCount.withHasReplay, 0) } func testTrackingSDKErrors() throws { @@ -210,6 +213,53 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { "It should report TOP 5 error kinds" ) } + + func testTrackingNTPOffset() throws { + let offsetAtStart: TimeInterval = .mockRandom(min: -10, max: 10) + let offsetAtEnd: TimeInterval = .mockRandom(min: -10, max: 10) + + core.context.serverTimeOffset = offsetAtStart + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = RUMMonitor.shared(in: core) + monitor.startView(key: "key", name: "View") + + // When + core.context.serverTimeOffset = offsetAtEnd + monitor.stopSession() + + // Then + let metric = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()) + XCTAssertEqual(metric.attributes?.ntpOffset.atStart, offsetAtStart.toInt64Milliseconds) + XCTAssertEqual(metric.attributes?.ntpOffset.atEnd, offsetAtEnd.toInt64Milliseconds) + } + + func testTrackingNoViewEventsCount() throws { + let expectedCount: Int = .mockRandom(min: 1, max: 5) + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = RUMMonitor.shared(in: core) + monitor.startView(key: "key", name: "View") + monitor.stopView(key: "key") // no active view + + // When + (0.. Date? { + try readWriteQueue.sync { + let file = try directory.coreDirectory.mostRecentModifiedFile(before: before) + return try file?.modifiedAt() + } + } +} +#if SPM_BUILD +import DatadogPrivate +#endif + +internal let registerObjcExceptionHandlerOnce: () -> Void = { + ObjcException.rethrow = __dd_private_ObjcExceptionHandler.rethrow + return {} +}() diff --git a/DatadogCore/Sources/Core/Storage/Files/Directory.swift b/DatadogCore/Sources/Core/Storage/Files/Directory.swift index 76fdfd239b..1e8712adc3 100644 --- a/DatadogCore/Sources/Core/Storage/Files/Directory.swift +++ b/DatadogCore/Sources/Core/Storage/Files/Directory.swift @@ -7,8 +7,15 @@ import Foundation import DatadogInternal +/// Provides interfaces for accessing common properties and operations for a directory. +internal protocol DirectoryProtocol: FileProtocol { + /// Returns list of subdirectories in the directory. + /// - Returns: list of subdirectories. + func subdirectories() throws -> [Directory] +} + /// An abstraction over file system directory where SDK stores its files. -internal struct Directory { +internal struct Directory: DirectoryProtocol { let url: URL /// Creates subdirectory with given path under system caches directory. @@ -21,6 +28,56 @@ internal struct Directory { self.url = url } + func modifiedAt() throws -> Date? { + try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date + } + + /// Returns list of subdirectories using system APIs. + /// - Returns: list of subdirectories. + func subdirectories() throws -> [Directory] { + try FileManager.default + .contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey, .canonicalPathKey]) + .filter { url in + var isDirectory = ObjCBool(false) + FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return isDirectory.boolValue + } + .map { url in Directory(url: url) } + } + + /// Recursively goes through subdirectories and finds the most recent modified file before given date. + /// This includes files in subdirectories, files in this directory and itself. + /// - Parameter before: The date to compare the last modification date of files. + /// - Returns: The latest modified file or `nil` if no files were modified before given date. + func mostRecentModifiedFile(before: Date) throws -> FileProtocol? { + let mostRecentModifiedInSubdirectories = try subdirectories() + .compactMap { directory in + try directory.mostRecentModifiedFile(before: before) + } + .max { file1, file2 in + guard let modifiedAt1 = try file1.modifiedAt(), let modifiedAt2 = try file2.modifiedAt() else { + return false + } + return modifiedAt1 < modifiedAt2 + } + + let files = try self.files() + + return try ([self, mostRecentModifiedInSubdirectories].compactMap { $0 } + files) + .filter { + guard let modifiedAt = try $0.modifiedAt() else { + return false + } + return modifiedAt < before + } + .max { file1, file2 in + guard let modifiedAt1 = try file1.modifiedAt(), let modifiedAt2 = try file2.modifiedAt() else { + return false + } + return modifiedAt1 < modifiedAt2 + } + } + /// Creates subdirectory with given path by creating intermediate directories if needed. /// If directory already exists at given `path` it will be used, without being altered. func createSubdirectory(path: String) throws -> Directory { diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift index 0dce3b8f2a..e3498d498e 100644 --- a/DatadogCore/Sources/Core/Storage/Files/File.swift +++ b/DatadogCore/Sources/Core/Storage/Files/File.swift @@ -5,10 +5,18 @@ */ import Foundation +import DatadogInternal -#if SPM_BUILD -import DatadogPrivate -#endif +/// Provides interfaces for accessing common properties and operations for a file. +internal protocol FileProtocol { + /// URL of the file on the disk. + var url: URL { get } + + /// Returns the date when the file was last modified. Returns `nil` if the file does not exist. + /// If the file is created and never modified, the creation date is returned. + /// - Returns: The date when the file was last modified. + func modifiedAt() throws -> Date? +} /// Provides convenient interface for reading metadata and appending data to the file. internal protocol WritableFile { @@ -40,7 +48,7 @@ private enum FileError: Error { /// An immutable `struct` designed to provide optimized and thread safe interface for file manipulation. /// It doesn't own the file, which means the file presence is not guaranteed - the file can be deleted by OS at any time (e.g. due to memory pressure). -internal struct File: WritableFile, ReadableFile { +internal struct File: WritableFile, ReadableFile, FileProtocol { let url: URL let name: String @@ -49,6 +57,10 @@ internal struct File: WritableFile, ReadableFile { self.name = url.lastPathComponent } + func modifiedAt() throws -> Date? { + try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date + } + /// Appends given data at the end of this file. func append(data: Data) throws { let fileHandle = try FileHandle(forWritingTo: url) @@ -90,11 +102,11 @@ internal struct File: WritableFile, ReadableFile { private func legacyAppend(_ data: Data, to fileHandle: FileHandle) throws { defer { - try? objcExceptionHandler.rethrowToSwift { + try? objc_rethrow { fileHandle.closeFile() } } - try objcExceptionHandler.rethrowToSwift { + try objc_rethrow { fileHandle.seekToEndOfFile() fileHandle.write(data) } diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift index 52be47d9df..b4b3848b81 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -15,6 +15,7 @@ internal protocol FilesOrchestratorType: AnyObject { func delete(readableFile: ReadableFile, deletionReason: BatchDeletedMetric.RemovalReason) var ignoreFilesAgeWhenReading: Bool { get set } + var trackName: String { get } } /// Orchestrates files in a single directory. @@ -52,6 +53,10 @@ internal class FilesOrchestrator: FilesOrchestratorType { /// An extra information to include in metrics or `nil` if metrics should not be reported for this orchestrator. let metricsData: MetricsData? + var trackName: String { + metricsData?.trackName ?? "Unknown" + } + init( directory: Directory, performance: StoragePerformancePreset, diff --git a/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift b/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift index 22e29e688e..eb487709df 100644 --- a/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift +++ b/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift @@ -37,25 +37,44 @@ internal struct FileWriter: Writer { /// - value: Encodable value to write. /// - metadata: Encodable metadata to write. func write(value: T, metadata: M?) { - do { - var encoded: Data = .init() - if let metadata = metadata { + var encoded: Data = .init() + if let metadata = metadata { + do { let encodedMetadata = try encode(value: metadata, blockType: .eventMetadata) encoded.append(encodedMetadata) + } catch { + DD.logger.error("(\(orchestrator.trackName)) Failed to encode metadata", error: error) + telemetry.error("(\(orchestrator.trackName)) Failed to encode metadata", error: error) } + } + do { let encodedValue = try encode(value: value, blockType: .event) encoded.append(encodedValue) + } catch { + DD.logger.error("(\(orchestrator.trackName)) Failed to encode value", error: error) + telemetry.error("(\(orchestrator.trackName)) Failed to encode value", error: error) + return + } - // Make sure both event and event metadata are written to the same file. - // This is to avoid a situation where event is written to one file and event metadata to another. - // If this happens, the reader will not be able to match event with its metadata. - let writeSize = UInt64(encoded.count) - let file = try orchestrator.getWritableFile(writeSize: writeSize) + // Make sure both event and event metadata are written to the same file. + // This is to avoid a situation where event is written to one file and event metadata to another. + // If this happens, the reader will not be able to match event with its metadata. + let writeSize = UInt64(encoded.count) + let file: WritableFile + do { + file = try orchestrator.getWritableFile(writeSize: writeSize) + } catch { + DD.logger.error("(\(orchestrator.trackName)) Failed to get writable file for \(writeSize) bytes", error: error) + telemetry.error("(\(orchestrator.trackName)) Failed to get writable file for \(writeSize) bytes", error: error) + return + } + + do { try file.append(data: encoded) } catch { - DD.logger.error("Failed to write data", error: error) - telemetry.error("Failed to write data to file", error: error) + DD.logger.error("(\(orchestrator.trackName)) Failed to write \(writeSize) bytes to file", error: error) + telemetry.error("(\(orchestrator.trackName)) Failed to write \(writeSize) bytes to file", error: error) } } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 1b06cbf415..5e3a487ce1 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -401,6 +401,8 @@ public enum Datadog { throw ProgrammerError(description: "The '\(instanceName)' instance of SDK is already initialized.") } + registerObjcExceptionHandlerOnce() + let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) if debug { consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) diff --git a/DatadogCore/Sources/Utils/Globals.swift b/DatadogCore/Sources/Utils/Globals.swift deleted file mode 100644 index baeb96d304..0000000000 --- a/DatadogCore/Sources/Utils/Globals.swift +++ /dev/null @@ -1,14 +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 Foundation - -#if SPM_BUILD -import DatadogPrivate -#endif - -/// Exception handler rethrowing `NSExceptions` to Swift `NSError`. -internal var objcExceptionHandler = __dd_private_ObjcExceptionHandler() diff --git a/DatadogCore/Sources/Versioning.swift b/DatadogCore/Sources/Versioning.swift index 238c6822ab..018926618d 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.13.0" +internal let __sdkVersion = "2.14.0" diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift index 31f7f4e843..3d83cd5340 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/DirectoryTests.swift @@ -171,6 +171,156 @@ class DirectoryTests: XCTestCase { XCTAssertNoThrow(try destinationDirectory.file(named: "f3")) } + func testModifiedAt() throws { + // when directory is created + let before = Date.timeIntervalSinceReferenceDate - 1 + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + let creationDate = try directory.modifiedAt() + let after = Date.timeIntervalSinceReferenceDate + 1 + + XCTAssertNotNil(creationDate) + XCTAssertGreaterThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, before) + XCTAssertLessThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, after) + + // when directory is updated + let beforeModification = Date.timeIntervalSinceReferenceDate + _ = try directory.createFile(named: "file") + let modificationDate = try directory.modifiedAt() + let afterModification = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(modificationDate) + XCTAssertGreaterThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, beforeModification) + XCTAssertLessThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, afterModification) + } + + func testLatestModifiedFile_whenDirectoryEmpty_returnsSelf() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(directory.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFiles_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsSubdirectories_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFilesAndSubdirectories_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file3.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsFilesAndFileIsDeleted_returnsLatestModifiedFile() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + try file3.append(data: .mock(ofSize: 3)) + + try file2.delete() + + let modifiedFile = try directory.mostRecentModifiedFile(before: .init()) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(directory.url, modifiedFile?.url) + } + + func testLatestModifiedFile_givenBeforeDate_returnsLatestModifiedFileBeforeGivenDate() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let file1 = try directory.createFile(named: "file1") + let file2 = try directory.createFile(named: "file2") + let file3 = try directory.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + let beforeDate = Date() + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: beforeDate) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file2.url, modifiedFile?.url) + } + + func testLatestModifiedFile_whenDirectoryContainsSubdirectoriesAndFiles_givenBeforeDate_returnsLatestModifiedFileBeforeGivenDate() throws { + let directory = try Directory(withSubdirectoryPath: uniqueSubdirectoryName()) + defer { directory.delete() } + + let subdirectory1 = try directory.createSubdirectory(path: "subdirectory1") + let subdirectory2 = try directory.createSubdirectory(path: "subdirectory2") + let subdirectory3 = try directory.createSubdirectory(path: "subdirectory3") + + let file1 = try subdirectory1.createFile(named: "file1") + let file2 = try subdirectory2.createFile(named: "file2") + let file3 = try subdirectory3.createFile(named: "file3") + + try file1.append(data: .mock(ofSize: 1)) + try file2.append(data: .mock(ofSize: 2)) + let beforeDate = Date() + try file3.append(data: .mock(ofSize: 3)) + + let modifiedFile = try directory.mostRecentModifiedFile(before: beforeDate) + XCTAssertNotNil(modifiedFile) + XCTAssertEqual(file2.url, modifiedFile?.url) + } + // MARK: - Helpers private func uniqueSubdirectoryName() -> String { diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift index e4a1ce4efd..c97b1d10b5 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift @@ -88,4 +88,26 @@ class FileTests: XCTestCase { XCTAssertEqual((error as NSError).localizedDescription, "The file “file” doesn’t exist.") } } + + func testModifiedAt() throws { + // when file is created + let before = Date.timeIntervalSinceReferenceDate + let file = try directory.createFile(named: "file") + let creationDate = try file.modifiedAt() + let after = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(creationDate) + XCTAssertGreaterThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, before) + XCTAssertLessThanOrEqual(creationDate!.timeIntervalSinceReferenceDate, after) + + // when file is modified + let beforeModification = Date.timeIntervalSinceReferenceDate + try file.append(data: .mock(ofSize: 5)) + let modificationDate = try file.modifiedAt() + let afterModification = Date.timeIntervalSinceReferenceDate + + XCTAssertNotNil(modificationDate) + XCTAssertGreaterThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, beforeModification) + XCTAssertLessThanOrEqual(modificationDate!.timeIntervalSinceReferenceDate, afterModification) + } } diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift index aa3c8f0216..2483cbd254 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift @@ -149,7 +149,8 @@ class FileWriterTests: XCTestCase { maxObjectSize: 23 // 23 bytes is enough for TLV with {"key1":"value1"} JSON ), dateProvider: SystemDateProvider(), - telemetry: NOPTelemetry() + telemetry: NOPTelemetry(), + metricsData: .init(trackName: "rum", consentLabel: .mockAny(), uploaderPerformance: UploadPerformanceMock.noOp) ), encryption: nil, telemetry: NOPTelemetry() @@ -168,7 +169,7 @@ class FileWriterTests: XCTestCase { reader = try BatchDataBlockReader(input: directory.files()[0].stream()) blocks = try XCTUnwrap(reader.all()) XCTAssertEqual(blocks.count, 1) // same content as before - XCTAssertEqual(dd.logger.errorLog?.message, "Failed to write data") + XCTAssertEqual(dd.logger.errorLog?.message, "(rum) Failed to encode value") XCTAssertEqual(dd.logger.errorLog?.error?.message, "DataBlock length exceeds limit of 23 bytes") } @@ -181,7 +182,8 @@ class FileWriterTests: XCTestCase { directory: directory, performance: PerformancePreset.mockAny(), dateProvider: SystemDateProvider(), - telemetry: NOPTelemetry() + telemetry: NOPTelemetry(), + metricsData: .init(trackName: "rum", consentLabel: .mockAny(), uploaderPerformance: UploadPerformanceMock.noOp) ), encryption: nil, telemetry: NOPTelemetry() @@ -189,7 +191,7 @@ class FileWriterTests: XCTestCase { writer.write(value: FailingEncodableMock(errorMessage: "failed to encode `FailingEncodable`.")) - XCTAssertEqual(dd.logger.errorLog?.message, "Failed to write data") + XCTAssertEqual(dd.logger.errorLog?.message, "(rum) Failed to encode value") XCTAssertEqual(dd.logger.errorLog?.error?.message, "failed to encode `FailingEncodable`.") } @@ -202,7 +204,8 @@ class FileWriterTests: XCTestCase { directory: directory, performance: PerformancePreset.mockAny(), dateProvider: SystemDateProvider(), - telemetry: NOPTelemetry() + telemetry: NOPTelemetry(), + metricsData: .init(trackName: "rum", consentLabel: .mockAny(), uploaderPerformance: UploadPerformanceMock.noOp) ), encryption: nil, telemetry: NOPTelemetry() @@ -213,7 +216,7 @@ class FileWriterTests: XCTestCase { writer.write(value: ["won't be written"]) try? directory.files()[0].makeReadWrite() - XCTAssertEqual(dd.logger.errorLog?.message, "Failed to write data") + XCTAssertEqual(dd.logger.errorLog?.message, "(rum) Failed to write 26 bytes to file") XCTAssertTrue(dd.logger.errorLog!.error!.message.contains("You don’t have permission")) } diff --git a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift index 6691eab176..77ff5ae105 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift @@ -270,6 +270,7 @@ internal class NOPFilesOrchestrator: FilesOrchestratorType { func delete(readableFile: ReadableFile, deletionReason: BatchDeletedMetric.RemovalReason) { } var ignoreFilesAgeWhenReading = false + var trackName: String = "nop" } extension DataFormat { diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 97566f0494..864d6377a5 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -93,6 +93,8 @@ class CrashReportSenderMock: CrashReportSender { } var didSendCrashReport: (() -> Void)? + + func send(launch: DatadogInternal.LaunchReport) {} } class RUMCrashReceiverMock: FeatureMessageReceiver { diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index 809b84f07c..f947e7b863 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -91,6 +91,10 @@ internal class DatadogCoreProxy: DatadogCoreProtocol { func send(message: FeatureMessage, else fallback: @escaping () -> Void) { core.send(message: message, else: fallback) } + + func mostRecentModifiedFileAt(before: Date) throws -> Date? { + return try core.mostRecentModifiedFileAt(before: before) + } } extension DatadogCoreProxy { diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift deleted file mode 100644 index 2ba25d2d9a..0000000000 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift +++ /dev/null @@ -1,20 +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 Foundation -import DatadogCore - -class ObjcExceptionHandlerMock: __dd_private_ObjcExceptionHandler { - let error: Error - - init(throwingError: Error) { - self.error = throwingError - } - - override func rethrowToSwift(tryBlock: @escaping () -> Void) throws { - throw error - } -} diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index 9642415537..7c3e56acb1 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -152,6 +152,7 @@ struct RUMCommandMock: RUMCommand { var attributes: [AttributeKey: AttributeValue] = [:] var canStartBackgroundView = false var isUserInteraction = false + var missedEventType: SessionEndedMetric.MissedEventType? = nil } /// Creates random `RUMCommand` from available ones. @@ -200,14 +201,16 @@ extension RUMStartViewCommand: AnyMockable, RandomMockable { attributes: [AttributeKey: AttributeValue] = [:], identity: ViewIdentifier = .mockViewIdentifier(), name: String = .mockAny(), - path: String = .mockAny() + path: String = .mockAny(), + instrumentationType: SessionEndedMetric.ViewInstrumentationType = .manual ) -> RUMStartViewCommand { return RUMStartViewCommand( time: time, identity: identity, name: name, path: path, - attributes: attributes + attributes: attributes, + instrumentationType: instrumentationType ) } } @@ -726,7 +729,8 @@ extension RUMScopeDependencies { onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), viewCache: ViewCache = ViewCache(), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), - sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()) + sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()), + watchdogTermination: WatchdogTerminationMonitor? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: featureScope, @@ -744,7 +748,8 @@ extension RUMScopeDependencies { onSessionStart: onSessionStart, viewCache: viewCache, fatalErrorContext: fatalErrorContext, - sessionEndedMetric: sessionEndedMetric + sessionEndedMetric: sessionEndedMetric, + watchdogTermination: watchdogTermination ) } @@ -764,7 +769,8 @@ extension RUMScopeDependencies { onSessionStart: RUM.SessionListener? = nil, viewCache: ViewCache? = nil, fatalErrorContext: FatalErrorContextNotifying? = nil, - sessionEndedMetric: SessionEndedMetricController? = nil + sessionEndedMetric: SessionEndedMetricController? = nil, + watchdogTermination: WatchdogTerminationMonitor? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: self.featureScope, @@ -782,7 +788,8 @@ extension RUMScopeDependencies { onSessionStart: onSessionStart ?? self.onSessionStart, viewCache: viewCache ?? self.viewCache, fatalErrorContext: fatalErrorContext ?? self.fatalErrorContext, - sessionEndedMetric: sessionEndedMetric ?? self.sessionEndedMetric + sessionEndedMetric: sessionEndedMetric ?? self.sessionEndedMetric, + watchdogTermination: watchdogTermination ) } } diff --git a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift index 40473a017a..b798ef6ff1 100644 --- a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift @@ -139,11 +139,11 @@ class DDDatadogTests: XCTestCase { XCTAssertEqual(userInfo.current.id, "id") XCTAssertEqual(userInfo.current.name, "name") XCTAssertEqual(userInfo.current.email, "email") - let extraInfo = try XCTUnwrap(userInfo.current.extraInfo as? [String: AnyEncodable]) - XCTAssertEqual(extraInfo["attribute-int"]?.value as? Int, 42) - XCTAssertEqual(extraInfo["attribute-double"]?.value as? Double, 42.5) - XCTAssertEqual(extraInfo["attribute-string"]?.value as? String, "string value") - XCTAssertEqual(extraInfo["foo"]?.value as? String, "bar") + let extraInfo = userInfo.current.extraInfo + XCTAssertEqual(extraInfo["attribute-int"] as? Int, 42) + XCTAssertEqual(extraInfo["attribute-double"] as? Double, 42.5) + XCTAssertEqual(extraInfo["attribute-string"] as? String, "string value") + XCTAssertEqual(extraInfo["foo"] as? String, "bar") DDDatadog.setUserInfo(id: nil, name: nil, email: nil, extraInfo: [:]) XCTAssertNil(userInfo.current.id) diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift index 9cb9201af7..6468bbe483 100644 --- a/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift @@ -107,12 +107,18 @@ class DDRUMConfigurationTests: XCTestCase { } func testAppHangThreshold() { - let random: TimeInterval = .mockRandom() + let random: TimeInterval = .mockRandom(min: 0.01, max: .greatestFiniteMagnitude) objc.appHangThreshold = random XCTAssertEqual(objc.appHangThreshold, random) XCTAssertEqual(swift.appHangThreshold, random) } + func testAppHangThresholdDisable() { + objc.appHangThreshold = 0 + XCTAssertEqual(objc.appHangThreshold, 0) + XCTAssertEqual(swift.appHangThreshold, nil) + } + func testVitalsUpdateFrequency() { objc.vitalsUpdateFrequency = .frequent XCTAssertEqual(swift.vitalsUpdateFrequency, .frequent) diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift index 89c9f85c11..8f2a1659e9 100644 --- a/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift @@ -34,7 +34,7 @@ class DDRUMViewTests: XCTestCase { func testItCreatesSwiftRUMView() { let objcRUMView = DDRUMView(name: "name", attributes: ["foo": "bar"]) XCTAssertEqual(objcRUMView.swiftView.name, "name") - XCTAssertEqual((objcRUMView.swiftView.attributes["foo"] as? AnyEncodable)?.value as? String, "bar") + XCTAssertEqual(objcRUMView.swiftView.attributes["foo"] as? String, "bar") XCTAssertEqual(objcRUMView.name, "name") XCTAssertEqual(objcRUMView.attributes["foo"] as? String, "bar") } @@ -80,7 +80,7 @@ class DDRUMActionTests: XCTestCase { func testItCreatesSwiftRUMAction() { let objcRUMAction = DDRUMAction(name: "name", attributes: ["foo": "bar"]) XCTAssertEqual(objcRUMAction.swiftAction.name, "name") - XCTAssertEqual((objcRUMAction.swiftAction.attributes["foo"] as? AnyEncodable)?.value as? String, "bar") + XCTAssertEqual(objcRUMAction.swiftAction.attributes["foo"] as? String, "bar") XCTAssertEqual(objcRUMAction.name, "name") XCTAssertEqual(objcRUMAction.attributes["foo"] as? String, "bar") } diff --git a/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift index 0d61436c82..b74454275c 100644 --- a/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift +++ b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift @@ -8,11 +8,9 @@ import XCTest import DatadogCore class ObjcExceptionHandlerTests: XCTestCase { - private let exceptionHandler = __dd_private_ObjcExceptionHandler() - func testGivenNonThrowingCode_itDoesNotThrow() throws { var counter = 0 - try exceptionHandler.rethrowToSwift { counter += 1 } + try __dd_private_ObjcExceptionHandler.rethrow { counter += 1 } XCTAssertEqual(counter, 1) } @@ -23,7 +21,7 @@ class ObjcExceptionHandlerTests: XCTestCase { userInfo: ["user-info": "some"] ) - XCTAssertThrowsError(try exceptionHandler.rethrowToSwift { nsException.raise() }) { error in + XCTAssertThrowsError(try __dd_private_ObjcExceptionHandler.rethrow { nsException.raise() }) { error in XCTAssertEqual((error as NSError).domain, "name") XCTAssertEqual((error as NSError).code, 0) XCTAssertEqual((error as NSError).userInfo as? [String: String], ["user-info": "some"]) diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec index 95c9775171..e149328fc9 100644 --- a/DatadogCrashReporting.podspec +++ b/DatadogCrashReporting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCrashReporting" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index fe99801329..20848d3e0d 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -59,6 +59,7 @@ internal final class CrashReportingFeature: DatadogFeature { self.plugin.readPendingCrashReport { [weak self] crashReport in guard let self = self, let availableCrashReport = crashReport else { DD.logger.debug("No pending Crash found") + self?.sender.send(launch: .init(didCrash: false)) return false } @@ -67,10 +68,12 @@ internal final class CrashReportingFeature: DatadogFeature { guard let crashContext = availableCrashReport.context.flatMap({ self.decode(crashContextData: $0) }) else { // `CrashContext` is malformed and and cannot be read. Return `true` to let the crash reporter // purge this crash report as we are not able to process it respectively. + self.sender.send(launch: .init(didCrash: true)) return true } self.sender.send(report: availableCrashReport, with: crashContext) + self.sender.send(launch: .init(didCrash: true)) return true } } diff --git a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift index 0052cec46b..f9f15a2640 100644 --- a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift +++ b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift @@ -14,6 +14,12 @@ internal protocol CrashReportSender { /// - report: The crash report. /// - context: The crash context func send(report: DDCrashReport, with context: CrashContext) + + /// Send the launch report and context to integrations. + /// + /// - Parameters: + /// - launch: The launch report. + func send(launch: LaunchReport) } /// An object for sending crash reports on the Core message-bus. @@ -66,4 +72,12 @@ internal struct MessageBusSender: CrashReportSender { } ) } + + /// Send the launch report and context to integrations. + /// + /// - Parameters: + /// - launch: The launch report. + func send(launch: DatadogInternal.LaunchReport) { + core?.set(baggage: launch, forKey: LaunchReport.baggageKey) + } } diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index 46048a1330..cccacea351 100644 --- a/DatadogInternal.podspec +++ b/DatadogInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogInternal" - s.version = "2.13.0" + s.version = "2.14.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 f3a57a0236..12b4c4b5d6 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -15,13 +15,15 @@ public enum AppState: Codable, PassthroughAnyCodable { case inactive /// The app is running in the background. case background + /// The app is terminated. + case terminated /// If the app is running in the foreground - no matter if receiving events or not (i.e. being interrupted because of transitioning from background). public var isRunningInForeground: Bool { switch self { case .active, .inactive: return true - case .background: + case .background, .terminated: return false } } diff --git a/DatadogInternal/Sources/Context/DatadogContext.swift b/DatadogInternal/Sources/Context/DatadogContext.swift index 45d7802f0d..daabc0323d 100644 --- a/DatadogInternal/Sources/Context/DatadogContext.swift +++ b/DatadogInternal/Sources/Context/DatadogContext.swift @@ -69,7 +69,7 @@ public struct DatadogContext { public let sdkInitDate: Date /// Current device information. - public let device: DeviceInfo + public var device: DeviceInfo /// Current user information. public var userInfo: UserInfo? diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index 99b2f885b5..2616786601 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -31,13 +31,29 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { /// The architecture of the device public let architecture: String + /// The device is a simulator + public let isSimulator: Bool + + /// The vendor identifier of the device. + public let vendorId: String? + + /// Returns `true` if the debugger is attached. + public let isDebugging: Bool + + /// Returns system boot time since epoch. + public let systemBootTime: TimeInterval + public init( name: String, model: String, osName: String, osVersion: String, osBuildNumber: String?, - architecture: String + architecture: String, + isSimulator: Bool, + vendorId: String?, + isDebugging: Bool, + systemBootTime: TimeInterval ) { self.brand = "Apple" self.name = name @@ -46,6 +62,10 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { self.osVersion = osVersion self.osBuildNumber = osBuildNumber self.architecture = architecture + self.isSimulator = isSimulator + self.vendorId = vendorId + self.isDebugging = isDebugging + self.systemBootTime = systemBootTime } } @@ -62,17 +82,20 @@ extension DeviceInfo { /// - device: The `UIDevice` description. public init( processInfo: ProcessInfo = .processInfo, - device: UIDevice = .current + device: UIDevice = .current, + sysctl: SysctlProviding = Sysctl() ) { var architecture = "unknown" if let archInfo = NXGetLocalArchInfo()?.pointee { architecture = String(utf8String: archInfo.name) ?? "unknown" } - let build = try? Sysctl.osVersion() + let build = try? sysctl.osBuild() + let isDebugging = try? sysctl.isDebugging() + let systemBootTime = try? sysctl.systemBootTime() #if !targetEnvironment(simulator) - let model = try? Sysctl.model() + let model = try? sysctl.model() // Real iOS device self.init( name: device.model, @@ -80,7 +103,11 @@ extension DeviceInfo { osName: device.systemName, osVersion: device.systemVersion, osBuildNumber: build, - architecture: architecture + architecture: architecture, + isSimulator: false, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate ) #else let model = processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? device.model @@ -91,7 +118,11 @@ extension DeviceInfo { osName: device.systemName, osVersion: device.systemVersion, osBuildNumber: build, - architecture: architecture + architecture: architecture, + isSimulator: true, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate ) #endif } @@ -103,17 +134,24 @@ extension DeviceInfo { /// - processInfo: The current process information. extension DeviceInfo { public init( - processInfo: ProcessInfo = .processInfo + processInfo: ProcessInfo = .processInfo, + sysctl: SysctlProviding = Sysctl() ) { var architecture = "unknown" if let archInfo = NXGetLocalArchInfo()?.pointee { architecture = String(utf8String: archInfo.name) ?? "unknown" } - Host.current().name - let build = (try? Sysctl.osVersion()) ?? "" - let model = (try? Sysctl.model()) ?? "" + let build = (try? sysctl.osBuild()) ?? "" + let model = (try? sysctl.model()) ?? "" let systemVersion = processInfo.operatingSystemVersion + let systemBootTime = try? sysctl.systemBootTime() + let isDebugging = try? sysctl.isDebugging() +#if targetEnvironment(simulator) + let isSimulator = true +#else + let isSimulator = false +#endif self.init( name: model.components(separatedBy: CharacterSet.letters.inverted).joined(), @@ -121,7 +159,11 @@ extension DeviceInfo { osName: "macOS", osVersion: "\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)", osBuildNumber: build, - architecture: architecture + architecture: architecture, + isSimulator: isSimulator, + vendorId: nil, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate ) } } diff --git a/DatadogInternal/Sources/Context/Sysctl.swift b/DatadogInternal/Sources/Context/Sysctl.swift index 292c7309a6..2ab0b7d3f7 100644 --- a/DatadogInternal/Sources/Context/Sysctl.swift +++ b/DatadogInternal/Sources/Context/Sysctl.swift @@ -15,15 +15,38 @@ import Foundation +/// A `SysctlProviding` implementation that uses `Darwin.sysctl` to access system information. +public protocol SysctlProviding { + /// Returns model of the device. + func model() throws -> String + + /// Returns operating system version. + /// - Returns: Operating system version. + func osBuild() throws -> String + + /// Returns system boot time since epoch. + /// It stays same across app restarts and only changes on the operating system reboot. + /// - Returns: System boot time. + func systemBootTime() throws -> TimeInterval + + /// Returns `true` if the app is being debugged. + /// - Returns: `true` if the app is being debugged. + func isDebugging() throws -> Bool +} + /// A "static"-only namespace around a series of functions that operate on buffers returned from the `Darwin.sysctl` function -internal struct Sysctl { +public struct Sysctl: SysctlProviding { /// Possible errors. enum Error: Swift.Error { case unknown case malformedUTF8 + case malformedData case posixError(POSIXErrorCode) } + public init() { + } + /// Access the raw data for an array of sysctl identifiers. private static func data(for keys: [Int32]) throws -> [Int8] { return try keys.withUnsafeBufferPointer { keysPointer throws -> [Int8] in @@ -63,7 +86,7 @@ internal struct Sysctl { /// e.g. "MacPro4,1" or "iPhone8,1" /// NOTE: this is *corrected* on iOS devices to fetch hw.machine - static func model() throws -> String { + public func model() throws -> String { #if os(iOS) && !arch(x86_64) && !arch(i386) // iOS device && not Simulator return try Sysctl.string(for: [CTL_HW, HW_MACHINE]) #else @@ -71,8 +94,31 @@ internal struct Sysctl { #endif } + /// Returns the operating system build as a human-readable string. /// e.g. "15D21" or "13D20" - static func osVersion() throws -> String { + public func osBuild() throws -> String { try Sysctl.string(for: [CTL_KERN, KERN_OSVERSION]) } + + /// Returns the system uptime in seconds. + public func systemBootTime() throws -> TimeInterval { + let bootTime = try Sysctl.data(for: [CTL_KERN, KERN_BOOTTIME]) + let uptime = bootTime.withUnsafeBufferPointer { buffer -> timeval? in + buffer.baseAddress?.withMemoryRebound(to: timeval.self, capacity: 1) { $0.pointee } + } + guard let uptime = uptime else { + throw Error.malformedData + } + return TimeInterval(uptime.tv_sec) + } + + /// Returns `true` if the debugger is attached to the current process. + /// https://developer.apple.com/library/archive/qa/qa1361/_index.html + public func isDebugging() throws -> Bool { + var info = kinfo_proc() + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + var size = MemoryLayout.stride + _ = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0) + return (info.kp_proc.p_flag & P_TRACED) != 0 + } } diff --git a/DatadogInternal/Sources/DatadogCoreProtocol.swift b/DatadogInternal/Sources/DatadogCoreProtocol.swift index c9655e8192..6a2c8bd9a5 100644 --- a/DatadogInternal/Sources/DatadogCoreProtocol.swift +++ b/DatadogInternal/Sources/DatadogCoreProtocol.swift @@ -11,8 +11,7 @@ import Foundation /// /// Any reference to `DatadogCoreProtocol` must be captured as `weak` within a Feature. This is to avoid /// retain cycle of core holding the Feature and vice-versa. -public protocol DatadogCoreProtocol: AnyObject, MessageSending, BaggageSharing { - // TODO: RUM-3717 +public protocol DatadogCoreProtocol: AnyObject, MessageSending, BaggageSharing, Storage { // Remove `DatadogCoreProtocol` conformance to `MessageSending` and `BaggageSharing` once // all features are migrated to depend on `FeatureScope` interface. @@ -84,7 +83,7 @@ public protocol BaggageSharing { /// // Bar.swift /// core.scope(for: "bar").eventWriteContext { context, writer in /// if let baggage = context.baggages["key"] { - /// try { + /// do { /// // Try decoding context to expected type: /// let value: String = try baggage.decode() /// // If success, handle the `value`. @@ -308,6 +307,8 @@ public class NOPDatadogCore: DatadogCoreProtocol { public func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { } /// no-op public func send(message: FeatureMessage, else fallback: @escaping () -> Void) { } + /// no-op + public func mostRecentModifiedFileAt(before: Date) throws -> Date? { return nil } } public struct NOPFeatureScope: FeatureScope { diff --git a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift index 9e95de1bf2..89558283bd 100644 --- a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift +++ b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift @@ -41,6 +41,8 @@ public struct NOPFeatureMessageReceiver: FeatureMessageReceiver { } } +/// A receiver that combines multiple receivers. It will loop though receivers and stop on the first that is able to +/// consume the given message. public struct CombinedFeatureMessageReceiver: FeatureMessageReceiver { let receivers: [FeatureMessageReceiver] diff --git a/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift b/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift new file mode 100644 index 0000000000..139bc7df47 --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/LaunchReport.swift @@ -0,0 +1,31 @@ +/* + * 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 Foundation + +/// Launch report format supported by Datadog SDK. +public struct LaunchReport: Codable, PassthroughAnyCodable { + /// The key used to encode/decode the `LaunchReport` in `DatadogContext.baggages` + public static let baggageKey = "launch-report" + + /// Returns `true` if the previous session crashed. + public let didCrash: Bool + + /// Creates a new `LaunchReport`. + /// - Parameter didCrash: `true` if the previous session crashed. + public init(didCrash: Bool) { + self.didCrash = didCrash + } +} + +extension LaunchReport: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + LaunchReport + - didCrash: \(didCrash) + """ + } +} diff --git a/DatadogInternal/Sources/Storage.swift b/DatadogInternal/Sources/Storage.swift new file mode 100644 index 0000000000..e5a51ca0a1 --- /dev/null +++ b/DatadogInternal/Sources/Storage.swift @@ -0,0 +1,37 @@ +/* + * 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 Foundation + +/// A Datadog protocol that provides persistance related information. +public protocol Storage { + /// Returns the most recent modified file before a given date. + /// - Parameter before: The date to compare the last modification date of files. + /// - Returns: The most recent modified file or `nil` if no files were modified before the given date. + func mostRecentModifiedFileAt(before: Date) throws -> Date? +} + +internal struct CoreStorage: Storage { + /// A weak core reference. + private weak var core: DatadogCoreProtocol? + + /// Creates a Storage associated with a core instance. + /// + /// The `CoreStorage` keeps a weak reference + /// to the provided core. + /// + /// - Parameter core: The core instance. + init(core: DatadogCoreProtocol) { + self.core = core + } + + /// Returns the most recent modified file before a given date from the core. + /// - Parameter before: The date to compare the last modification date of files. + /// - Returns: The most recent modified file or `nil` if no files were modified before the given date. + func mostRecentModifiedFileAt(before: Date) throws -> Date? { + try core?.mostRecentModifiedFileAt(before: before) + } +} diff --git a/DatadogInternal/Sources/Telemetry/Telemetry.swift b/DatadogInternal/Sources/Telemetry/Telemetry.swift index 5f875d2eeb..e45a978995 100644 --- a/DatadogInternal/Sources/Telemetry/Telemetry.swift +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -395,6 +395,12 @@ extension DatadogCoreProtocol { public var telemetry: Telemetry { CoreTelemetry(core: self) } } +extension DatadogCoreProtocol { + /// Provides access to the `Storage` associated with the core. + /// - Returns: The `Storage` instance. + public var storage: Storage { CoreStorage(core: self) } +} + extension ConfigurationTelemetry { public func merged(with other: Self) -> Self { .init( diff --git a/DatadogInternal/Sources/Utils/DDError.swift b/DatadogInternal/Sources/Utils/DDError.swift index 1d4b35ffd1..ca40d8a63f 100644 --- a/DatadogInternal/Sources/Utils/DDError.swift +++ b/DatadogInternal/Sources/Utils/DDError.swift @@ -90,3 +90,52 @@ public struct InternalError: Error, CustomStringConvertible { self.description = description } } + +public struct ObjcException: Error { + /// A closure to catch Objective-C runtime exception and rethrow as `Swift.Error`. + /// + /// - Important: Does nothing by default, it must be set to an Objective-C interopable function. + /// + /// - Warning: As stated in [Objective-C Automatic Reference Counting (ARC)](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions), + /// in Objective-C, ARC is not exception-safe and does not perform releases which would occur at the end of a + /// full-expression if that full-expression throws an exception. Therefore, ARC-generated code leaks by default + /// on exceptions. + public static var rethrow: ((() -> Void) throws -> Void) = { $0() } + + /// The underlying `NSError` describing the `NSException` + /// thrown by Objective-C runtime. + public let error: Error + /// The source file in which the exception was raised. + public let file: String + /// The line number on which the exception was raised. + public let line: Int +} + +/// Rethrow Objective-C runtime exception as `Swift.Error`. +/// +/// - Warning: As stated in [Objective-C Automatic Reference Counting (ARC)](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions), +/// in Objective-C, ARC is not exception-safe and does not perform releases which would occur at the end of a +/// full-expression if that full-expression throws an exception. Therefore, ARC-generated code leaks by default +/// on exceptions. +/// - throws: `ObjcException` if an exception was raised by the Objective-C runtime. +@discardableResult +public func objc_rethrow(_ block: () throws -> T, file: String = #fileID, line: Int = #line) throws -> T { + var value: T! //swiftlint:disable:this implicitly_unwrapped_optional + var swiftError: Error? + do { + try ObjcException.rethrow { + do { + value = try block() + } catch { + swiftError = error + } + } + } catch { + // wrap the underlying objc runtime exception in + // a `ObjcException` for easier matching during + // escalation. + throw ObjcException(error: error, file: file, line: line) + } + + return try swiftError.map { throw $0 } ?? value +} diff --git a/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift b/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift new file mode 100644 index 0000000000..8c2c9e3a0e --- /dev/null +++ b/DatadogInternal/Tests/Utils/ObjcExceptionTests.swift @@ -0,0 +1,45 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogInternal + +class ObjcExceptionTests: XCTestCase { + func testWrappedObjcException() { + // Given + ObjcException.rethrow = { _ in throw ErrorMock("objc exception") } + defer { ObjcException.rethrow = { $0() } } + + do { + #sourceLocation(file: "File.swift", line: 1) + try objc_rethrow {} + #sourceLocation() + XCTFail("objc_rethrow should throw an error") + } catch let exception as ObjcException { + let error = exception.error as? ErrorMock + XCTAssertEqual(error?.description, "objc exception") + XCTAssertEqual(exception.file, "\(moduleName())/File.swift") + XCTAssertEqual(exception.line, 1) + } catch { + XCTFail("error should be of type ObjcException") + } + } + + func testRethrowSwiftError() { + do { + try objc_rethrow { throw ErrorMock("swift error") } + XCTFail("objc_rethrow should throw an error") + } catch let error as ErrorMock { + XCTAssertEqual(error.description, "swift error") + } catch is ObjcException { + XCTFail("error should not be of type ObjcException") + } catch { + XCTFail("error should be of type ErrorMock") + } + } +} diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index 110ed51c2a..61445c6cb7 100644 --- a/DatadogLogs.podspec +++ b/DatadogLogs.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogLogs" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Datadog Logs Module." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogObjc.podspec b/DatadogObjc.podspec index 1899de9aa7..f256756b32 100644 --- a/DatadogObjc.podspec +++ b/DatadogObjc.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogObjc" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift b/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift index a5a3f160f9..06dcb60392 100644 --- a/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift +++ b/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift @@ -10,19 +10,16 @@ import DatadogCore /// Casts `[String: Any]` attributes to their `Encodable` representation by wrapping each `Any` into `AnyEncodable`. internal func castAttributesToSwift(_ attributes: [String: Any]) -> [String: Encodable] { - return attributes.mapValues { AnyEncodable($0) } + attributes.mapValues { $0 as? Encodable ?? AnyEncodable($0) } } /// Casts `[String: Encodable]` attributes to their `Any` representation by unwrapping each `AnyEncodable` into `Any`. internal func castAttributesToObjectiveC(_ attributes: [String: Encodable]) -> [String: Any] { - return attributes - .compactMapValues { value in (value as? AnyEncodable)?.value } + attributes.mapValues { ($0 as? AnyEncodable).map(\.value) ?? $0 } } /// Helper extension to use `castAttributesToObjectiveC(_:)` in auto generated ObjC interop `RUMDataModels`. /// Unlike the function it wraps, it has postfix notation which makes it easier to use in generated code. internal extension Dictionary where Key == String, Value == Encodable { - func castToObjectiveC() -> [String: Any] { - return castAttributesToObjectiveC(self) - } + func castToObjectiveC() -> [String: Any] { castAttributesToObjectiveC(self) } } diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift index 6f2e8cd113..fee8ba2561 100644 --- a/DatadogObjc/Sources/RUM/RUM+objc.swift +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -372,13 +372,18 @@ public class DDRUMConfiguration: NSObject { get { swiftConfig.trackBackgroundEvents } } + @objc public var trackWatchdogTerminations: Bool { + set { swiftConfig.trackWatchdogTerminations = newValue } + get { swiftConfig.trackWatchdogTerminations } + } + @objc public var longTaskThreshold: TimeInterval { set { swiftConfig.longTaskThreshold = newValue } get { swiftConfig.longTaskThreshold ?? 0 } } @objc public var appHangThreshold: TimeInterval { - set { swiftConfig.appHangThreshold = newValue } + set { swiftConfig.appHangThreshold = newValue == 0 ? nil : newValue } get { swiftConfig.appHangThreshold ?? 0 } } diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 283d4b9530..30453ce997 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -664,6 +664,7 @@ public enum DDRUMActionEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -676,6 +677,7 @@ public enum DDRUMActionEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -686,6 +688,7 @@ public enum DDRUMActionEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -889,6 +892,7 @@ public enum DDRUMActionEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -902,6 +906,7 @@ public enum DDRUMActionEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -913,6 +918,7 @@ public enum DDRUMActionEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -1447,6 +1453,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -1459,6 +1466,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -1469,6 +1477,7 @@ public enum DDRUMErrorEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -1722,6 +1731,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case .aNR?: self = .aNR case .appHang?: self = .appHang case .exception?: self = .exception + case .watchdogTermination?: self = .watchdogTermination } } @@ -1731,6 +1741,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case .aNR: return .aNR case .appHang: return .appHang case .exception: return .exception + case .watchdogTermination: return .watchdogTermination } } @@ -1738,6 +1749,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case aNR case appHang case exception + case watchdogTermination } @objc @@ -2264,6 +2276,7 @@ public enum DDRUMErrorEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -2277,6 +2290,7 @@ public enum DDRUMErrorEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -2288,6 +2302,7 @@ public enum DDRUMErrorEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -2818,6 +2833,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -2830,6 +2846,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -2840,6 +2857,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -3064,6 +3082,7 @@ public enum DDRUMLongTaskEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -3077,6 +3096,7 @@ public enum DDRUMLongTaskEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -3088,6 +3108,7 @@ public enum DDRUMLongTaskEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -3626,6 +3647,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -3638,6 +3660,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -3648,6 +3671,7 @@ public enum DDRUMResourceEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -4283,6 +4307,7 @@ public enum DDRUMResourceEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -4296,6 +4321,7 @@ public enum DDRUMResourceEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -4307,6 +4333,7 @@ public enum DDRUMResourceEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -4877,6 +4904,7 @@ public enum DDRUMViewEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -4889,6 +4917,7 @@ public enum DDRUMViewEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -4899,6 +4928,7 @@ public enum DDRUMViewEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -5188,6 +5218,7 @@ public enum DDRUMViewEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -5201,6 +5232,7 @@ public enum DDRUMViewEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -5212,6 +5244,7 @@ public enum DDRUMViewEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -6096,6 +6129,7 @@ public enum DDRUMVitalEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6108,6 +6142,7 @@ public enum DDRUMVitalEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6118,6 +6153,7 @@ public enum DDRUMVitalEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -6321,6 +6357,7 @@ public enum DDRUMVitalEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -6334,6 +6371,7 @@ public enum DDRUMVitalEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6345,6 +6383,7 @@ public enum DDRUMVitalEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -6433,6 +6472,14 @@ public class DDRUMVitalEventVital: NSObject { root.swiftModel.vital.custom as [String: NSNumber]? } + @objc public var details: String? { + root.swiftModel.vital.details + } + + @objc public var duration: NSNumber? { + root.swiftModel.vital.duration as NSNumber? + } + @objc public var id: String { root.swiftModel.vital.id } @@ -6583,6 +6630,7 @@ public enum DDTelemetryErrorEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6594,6 +6642,7 @@ public enum DDTelemetryErrorEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6603,6 +6652,7 @@ public enum DDTelemetryErrorEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -6834,6 +6884,7 @@ public enum DDTelemetryDebugEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6845,6 +6896,7 @@ public enum DDTelemetryDebugEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6854,6 +6906,7 @@ public enum DDTelemetryDebugEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -7064,6 +7117,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -7075,6 +7129,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -7084,6 +7139,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -7196,6 +7252,10 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { get { root.swiftModel.telemetry.configuration.mobileVitalsUpdatePeriod as NSNumber? } } + @objc public var plugins: [DDTelemetryConfigurationEventTelemetryConfigurationPlugins]? { + root.swiftModel.telemetry.configuration.plugins?.map { DDTelemetryConfigurationEventTelemetryConfigurationPlugins(swiftModel: $0) } + } + @objc public var premiumSampleRate: NSNumber? { root.swiftModel.telemetry.configuration.premiumSampleRate as NSNumber? } @@ -7465,6 +7525,24 @@ public class DDTelemetryConfigurationEventTelemetryConfigurationForwardReports: } } +@objc +public class DDTelemetryConfigurationEventTelemetryConfigurationPlugins: NSObject { + internal var swiftModel: TelemetryConfigurationEvent.Telemetry.Configuration.Plugins + internal var root: DDTelemetryConfigurationEventTelemetryConfigurationPlugins { self } + + internal init(swiftModel: TelemetryConfigurationEvent.Telemetry.Configuration.Plugins) { + self.swiftModel = swiftModel + } + + @objc public var name: String { + root.swiftModel.name + } + + @objc public var pluginsInfo: [String: Any] { + root.swiftModel.pluginsInfo.castToObjectiveC() + } +} + @objc public enum DDTelemetryConfigurationEventTelemetryConfigurationSelectedTracingPropagators: Int { internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.SelectedTracingPropagators?) { @@ -7629,4 +7707,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/30d4b773abb4e33edc9d6053d3c12cd302e948a5 +// Generated from https://github.com/DataDog/rum-events-format/tree/ae8c30a094339995e234fd55831ade0999bf0612 diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec index ae25d3ddcf..0e82d988eb 100644 --- a/DatadogRUM.podspec +++ b/DatadogRUM.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogRUM" - s.version = "2.13.0" + s.version = "2.14.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 c6ee571394..bb1b5fe084 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -358,6 +358,7 @@ public struct RUMActionEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -422,6 +423,7 @@ public struct RUMActionEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -650,6 +652,7 @@ public struct RUMErrorEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -803,6 +806,7 @@ public struct RUMErrorEvent: RUMDataModel { case aNR = "ANR" case appHang = "App Hang" case exception = "Exception" + case watchdogTermination = "Watchdog Termination" } /// Properties for one of the error causes @@ -1043,6 +1047,7 @@ public struct RUMErrorEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -1291,6 +1296,7 @@ public struct RUMLongTaskEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -1373,6 +1379,7 @@ public struct RUMLongTaskEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -1605,6 +1612,7 @@ public struct RUMResourceEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -1921,6 +1929,7 @@ public struct RUMResourceEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -2184,6 +2193,7 @@ public struct RUMViewEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -2304,6 +2314,7 @@ public struct RUMViewEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -2866,6 +2877,7 @@ public struct RUMVitalEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// Attributes of the view's container @@ -2930,6 +2942,7 @@ public struct RUMVitalEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -2959,6 +2972,12 @@ public struct RUMVitalEvent: RUMDataModel { /// User custom vital. public let custom: [String: Double]? + /// Details of the vital. It can be used as a secondary identifier (URL, React component name...) + public let details: String? + + /// Duration of the vital in nanoseconds + public let duration: Double? + /// UUID of the vital public let id: String @@ -2970,6 +2989,8 @@ public struct RUMVitalEvent: RUMDataModel { enum CodingKeys: String, CodingKey { case custom = "custom" + case details = "details" + case duration = "duration" case id = "id" case name = "name" case type = "type" @@ -3083,6 +3104,7 @@ public struct TelemetryErrorEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// The telemetry log information @@ -3282,6 +3304,7 @@ public struct TelemetryDebugEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// The telemetry log information @@ -3461,6 +3484,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case flutter = "flutter" case reactNative = "react-native" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// The telemetry configuration information @@ -3539,6 +3563,9 @@ public struct TelemetryConfigurationEvent: RUMDataModel { /// The period between each Mobile Vital sample (in milliseconds) public var mobileVitalsUpdatePeriod: Int64? + /// The list of plugins enabled + public internal(set) 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? @@ -3704,6 +3731,7 @@ public struct TelemetryConfigurationEvent: RUMDataModel { case forwardReports = "forward_reports" case initializationType = "initialization_type" case mobileVitalsUpdatePeriod = "mobile_vitals_update_period" + case plugins = "plugins" case premiumSampleRate = "premium_sample_rate" case reactNativeVersion = "react_native_version" case reactVersion = "react_version" @@ -3839,6 +3867,17 @@ public struct TelemetryConfigurationEvent: RUMDataModel { } } + public struct Plugins: Codable { + /// The name of the plugin + public let name: String + + public internal(set) var pluginsInfo: [String: Encodable] + + enum StaticCodingKeys: String, CodingKey { + case name = "name" + } + } + public enum SelectedTracingPropagators: String, Codable { case datadog = "datadog" case b3 = "b3" @@ -3917,6 +3956,39 @@ extension TelemetryConfigurationEvent.Telemetry { } } +extension TelemetryConfigurationEvent.Telemetry.Configuration.Plugins { + public func encode(to encoder: Encoder) throws { + // Encode static properties: + var staticContainer = encoder.container(keyedBy: StaticCodingKeys.self) + try staticContainer.encodeIfPresent(name, forKey: .name) + + // Encode dynamic properties: + var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) + try pluginsInfo.forEach { + let key = DynamicCodingKey($0) + try dynamicContainer.encode(AnyEncodable($1), forKey: key) + } + } + + public init(from decoder: Decoder) throws { + // Decode static properties: + let staticContainer = try decoder.container(keyedBy: StaticCodingKeys.self) + self.name = try staticContainer.decode(String.self, forKey: .name) + + // Decode other properties into [String: Codable] dictionary: + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + let allStaticKeys = Set(staticContainer.allKeys.map { $0.stringValue }) + let dynamicKeys = dynamicContainer.allKeys.filter { !allStaticKeys.contains($0.stringValue) } + var dictionary: [String: Codable] = [:] + + try dynamicKeys.forEach { codingKey in + dictionary[codingKey.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: codingKey) + } + + self.pluginsInfo = dictionary + } +} + /// The precondition that led to the creation of the session public enum RUMSessionPrecondition: String, Codable { case userAppLaunch = "user_app_launch" @@ -4262,4 +4334,4 @@ public struct RUMTelemetryOperatingSystem: Codable { } } -// Generated from https://github.com/DataDog/rum-events-format/tree/30d4b773abb4e33edc9d6053d3c12cd302e948a5 +// Generated from https://github.com/DataDog/rum-events-format/tree/ae8c30a094339995e234fd55831ade0999bf0612 diff --git a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift index f08bdf92ee..0baff0367b 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift @@ -56,6 +56,7 @@ internal extension RUMViewEvent.Source { case .flutter: return .flutter case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } } @@ -73,6 +74,10 @@ internal extension RUMViewEvent { } } + enum EventType: String, Codable { + case view + } + /// Creates `Metadata` from the given `RUMViewEvent`. /// - Returns: The `Metadata` for the given `RUMViewEvent`. func metadata() -> Metadata { diff --git a/DatadogRUM/Sources/FatalErrorBuilder.swift b/DatadogRUM/Sources/FatalErrorBuilder.swift index be78a263a7..65915a5da8 100644 --- a/DatadogRUM/Sources/FatalErrorBuilder.swift +++ b/DatadogRUM/Sources/FatalErrorBuilder.swift @@ -19,11 +19,14 @@ internal struct FatalErrorBuilder { static let viewEventAvailabilityThreshold: TimeInterval = 14_400 // 4 hours } + /// Fatal error types. enum FatalError { /// A crash with given metadata information. case crash /// A fatal App Hang. case hang + /// A crash caused by operating system watchdog. + case watchdogTermination } /// Current SDK context. @@ -34,7 +37,7 @@ internal struct FatalErrorBuilder { let errorDate: Date let errorType: String let errorMessage: String - let errorStack: String + let errorStack: String? let errorThreads: [RUMErrorEvent.Error.Threads]? let errorBinaryImages: [RUMErrorEvent.Error.BinaryImages]? @@ -77,6 +80,7 @@ internal struct FatalErrorBuilder { switch error { case .crash: return .exception case .hang: return .appHang + case .watchdogTermination: return .watchdogTermination } }(), csp: nil, @@ -87,6 +91,7 @@ internal struct FatalErrorBuilder { switch error { case .crash: return true case .hang: return true // fatal hangs are considered `@error.is_crash: true` + case .watchdogTermination: return true } }(), message: errorMessage, @@ -162,6 +167,7 @@ internal struct FatalErrorBuilder { switch error { case .crash: return 1 case .hang: return 1 // fatal hangs are considered in `@view.crash.count` + case .watchdogTermination: return 1 } }() ), diff --git a/DatadogRUM/Sources/Feature/RUMDataStore.swift b/DatadogRUM/Sources/Feature/RUMDataStore.swift index dd3996d195..6ceae69f64 100644 --- a/DatadogRUM/Sources/Feature/RUMDataStore.swift +++ b/DatadogRUM/Sources/Feature/RUMDataStore.swift @@ -30,6 +30,8 @@ internal struct RUMDataStore { /// References pending App Hang information. /// If found during app start it is considered a fatal hang in previous process. case fatalAppHangKey = "fatal-app-hang" + case watchdogAppStateKey = "watchdog-app-state" + case watchdogRUMViewEvent = "watchdog-rum-view-event" } /// Encodes values in RUM data store. @@ -45,7 +47,7 @@ internal struct RUMDataStore { let data = try RUMDataStore.encoder.encode(value) featureScope.dataStore.setValue(data, forKey: key.rawValue, version: version) } catch let error { - DD.logger.error("Failed to encode \(type(of: value)) in RUM Data Store") + DD.logger.error("Failed to encode \(type(of: value)) in RUM Data Store", error: error) featureScope.telemetry.error("Failed to encode \(type(of: value)) in RUM Data Store", error: error) } } @@ -64,7 +66,7 @@ internal struct RUMDataStore { let value = try RUMDataStore.decoder.decode(V.self, from: data) callback(value) } catch let error { - DD.logger.error("Failed to decode \(V.self) from RUM Data Store") + DD.logger.error("Failed to decode \(V.self) from RUM Data Store", error: error) featureScope.telemetry.error("Failed to decode \(V.self) from RUM Data Store", error: error) callback(nil) } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index ead9764c04..1f4fab6f45 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import UIKit internal final class RUMFeature: DatadogRemoteFeature { static let name = "rum" @@ -36,6 +37,30 @@ internal final class RUMFeature: DatadogRemoteFeature { let featureScope = core.scope(for: RUMFeature.self) let sessionEndedMetric = SessionEndedMetricController(telemetry: core.telemetry) + + var watchdogTermination: WatchdogTerminationMonitor? + if configuration.trackWatchdogTerminations { + let appStateManager = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: configuration.processID, + syntheticsEnvironment: configuration.syntheticsEnvironment + ) + let monitor = WatchdogTerminationMonitor( + appStateManager: appStateManager, + checker: .init( + appStateManager: appStateManager, + featureScope: featureScope + ), + stroage: core.storage, + feature: featureScope, + reporter: WatchdogTerminationReporter( + featureScope: featureScope, + dateProvider: configuration.dateProvider + ) + ) + watchdogTermination = monitor + } + let dependencies = RUMScopeDependencies( featureScope: featureScope, rumApplicationID: configuration.applicationID, @@ -74,7 +99,8 @@ internal final class RUMFeature: DatadogRemoteFeature { onSessionStart: configuration.onSessionStart, viewCache: ViewCache(), fatalErrorContext: FatalErrorContextNotifier(messageBus: featureScope), - sessionEndedMetric: sessionEndedMetric + sessionEndedMetric: sessionEndedMetric, + watchdogTermination: watchdogTermination ) self.monitor = Monitor( @@ -92,14 +118,15 @@ internal final class RUMFeature: DatadogRemoteFeature { dateProvider: configuration.dateProvider, backtraceReporter: core.backtraceReporter, fatalErrorContext: dependencies.fatalErrorContext, - processID: configuration.processID + processID: configuration.processID, + watchdogTermination: watchdogTermination ) self.requestBuilder = RequestBuilder( customIntakeURL: configuration.customEndpoint, eventsFilter: RUMViewEventsFilter(), telemetry: core.telemetry ) - self.messageReceiver = CombinedFeatureMessageReceiver( + var messageReceivers: [FeatureMessageReceiver] = [ TelemetryInterceptor(sessionEndedMetric: sessionEndedMetric), TelemetryReceiver( featureScope: featureScope, @@ -135,7 +162,13 @@ internal final class RUMFeature: DatadogRemoteFeature { }(), eventsMapper: eventsMapper ) - ) + ] + + if let watchdogTermination = watchdogTermination { + messageReceivers.append(watchdogTermination) + } + + self.messageReceiver = CombinedFeatureMessageReceiver(messageReceivers) // Forward instrumentation calls to monitor: instrumentation.publish(to: monitor) diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 13fc7b0bf4..8b86fb09c6 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -6,6 +6,7 @@ import Foundation import DatadogInternal +import UIKit /// Bundles RUM instrumentation components. internal final class RUMInstrumentation: RUMCommandPublisher { @@ -36,6 +37,9 @@ internal final class RUMInstrumentation: RUMCommandPublisher { /// Instruments App Hangs. It is `nil` if hangs monitoring is not enabled. let appHangs: AppHangsMonitor? + /// Instruments Watchdog Terminations. + let watchdogTermination: WatchdogTerminationMonitor? + // MARK: - Initialization init( @@ -48,7 +52,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { dateProvider: DateProvider, backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, - processID: UUID + processID: UUID, + watchdogTermination: WatchdogTerminationMonitor? ) { // 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: @@ -119,6 +124,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { self.uiApplicationSwizzler = uiApplicationSwizzler self.longTasks = longTasks self.appHangs = appHangs + self.watchdogTermination = watchdogTermination // Enable configured instrumentations: self.viewControllerSwizzler?.swizzle() @@ -133,6 +139,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { uiApplicationSwizzler?.unswizzle() longTasks?.stop() appHangs?.stop() + watchdogTermination?.stop() } func publish(to subscriber: RUMCommandSubscriber) { diff --git a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift index 5d805a4319..770e4d1af8 100644 --- a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift @@ -25,6 +25,9 @@ internal final class RUMViewsHandler { /// Custom attributes to attach to the View. let attributes: [AttributeKey: AttributeValue] + + /// The type of instrumentation that started this view. + let instrumentationType: SessionEndedMetric.ViewInstrumentationType } /// The current date provider. @@ -157,7 +160,8 @@ internal final class RUMViewsHandler { identity: view.identity, name: view.name, path: view.path, - attributes: view.attributes + attributes: view.attributes, + instrumentationType: view.instrumentationType ) ) } @@ -205,7 +209,8 @@ extension RUMViewsHandler: UIViewControllerHandler { name: rumView.name, path: rumView.path ?? viewController.canonicalClassName, isUntrackedModal: rumView.isUntrackedModal, - attributes: rumView.attributes + attributes: rumView.attributes, + instrumentationType: .uikit ) ) } else if #available(iOS 13, tvOS 13, *), viewController.isModalInPresentation { @@ -215,7 +220,8 @@ extension RUMViewsHandler: UIViewControllerHandler { name: "RUMUntrackedModal", path: viewController.canonicalClassName, isUntrackedModal: true, - attributes: [:] + attributes: [:], + instrumentationType: .uikit ) ) } @@ -240,7 +246,8 @@ extension RUMViewsHandler: SwiftUIViewHandler { name: name, path: path, isUntrackedModal: false, - attributes: attributes + attributes: attributes, + instrumentationType: .swiftui ) ) } diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift new file mode 100644 index 0000000000..2d9be30366 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppState.swift @@ -0,0 +1,65 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +#if canImport(UIKit) +import UIKit +#endif + +/// Represents the app state observed during application lifecycle events such as application start, resume and termination. +/// This state is used to detect Watchdog Terminations. +internal struct WatchdogTerminationAppState: Codable { + /// The Application version provided by the `Bundle`. + let appVersion: String + + /// The Operating System version. + let osVersion: String + + /// Last time the system was booted. + let systemBootTime: TimeInterval + + /// Returns true, if the app is running with a debug configuration. + let isDebugging: Bool + + /// Returns true, if the app was terminated normally. + var wasTerminated: Bool + + /// Returns true, if the app was in the foreground. + var isActive: Bool + + /// The vendor identifier of the device. + /// This value can change when installing test builds using Xcode or when installing an app on a device using ad-hoc distribution. + let vendorId: String? + + /// The process identifier of the app. This value stays the same during SDK start and stop but the app stays in memory. + let processId: UUID + + /// The user's tracking consent at the recoding time. + let trackingConsent: TrackingConsent + + /// Returns true, if the app is running in a synthetic environment. + let syntheticsEnvironment: Bool +} + +extension WatchdogTerminationAppState: CustomDebugStringConvertible { + var debugDescription: String { + return """ + WatchdogTerminationAppState + - appVersion: \(appVersion) + - osVersion: \(osVersion) + - systemBootTime: \(systemBootTime) + - isDebugging: \(isDebugging) + - wasTerminated: \(wasTerminated) + - isActive: \(isActive) + - vendorId: \(vendorId ?? "nil") + - processId: \(processId) + - trackingConsent: \(trackingConsent) + - syntheticsEnvironment: \(syntheticsEnvironment) + """ + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift new file mode 100644 index 0000000000..cde049cc97 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManager.swift @@ -0,0 +1,116 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Manages the app state changes observed during application lifecycle events such as application start, resume and termination. +internal final class WatchdogTerminationAppStateManager { + let featureScope: FeatureScope + + /// The last app state observed during application lifecycle events. + @ReadWriteLock + var lastAppState: AppState? + + /// The process identifier of the app whose state is being monitored. + let processId: UUID + + /// Returns true, if the app is running in a synthetic environment. + let syntheticsEnvironment: Bool + + init( + featureScope: FeatureScope, + processId: UUID, + syntheticsEnvironment: Bool + ) { + self.featureScope = featureScope + self.processId = processId + self.syntheticsEnvironment = syntheticsEnvironment + } + + /// Deletes the app state from the data store. + func deleteAppState() { + DD.logger.debug("Deleting app state from data store") + featureScope.rumDataStore.removeValue(forKey: .watchdogAppStateKey) + } + + /// Updates the app state based on the given application state. + /// + /// For watchdog termination, we are interested in + /// 1. whether the application was terminated using conventions methods or not. + /// 2. whether the application was in the foreground or background when it was terminated. + /// + /// - Parameter state: The application state. + func updateAppState(state: AppState) { + // this method can be called multiple times for the same state, + // so we need to make sure we don't update the state multiple times + guard state != lastAppState else { + return + } + switch state { + case .active: + updateAppState { state in + state?.isActive = true + } + case .inactive, .background: + updateAppState { state in + state?.isActive = false + } + case .terminated: + updateAppState { state in + state?.wasTerminated = true + } + } + lastAppState = state + } + + /// Updates the app state in the data store with the given block. + /// - Parameter block: The block to update the app state. + private func updateAppState(block: @escaping (inout WatchdogTerminationAppState?) -> Void) { + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + var appState = appState + block(&appState) + DD.logger.debug("Updating app state in data store") + self.featureScope.rumDataStore.setValue(appState, forKey: .watchdogAppStateKey) + } + } + + /// Builds the current app state and stores it in the data store. + func storeCurrentAppState() throws { + try currentAppState { [self] appState in + featureScope.rumDataStore.setValue(appState, forKey: .watchdogAppStateKey) + } + } + + /// Reads the app state from the data store asynchronously. + /// - Parameter completion: The completion block called with the app state. + func readAppState(completion: @escaping (WatchdogTerminationAppState?) -> Void) { + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (state: WatchdogTerminationAppState?) in + DD.logger.debug("Reading app state from data store.") + completion(state) + } + } + + /// Builds the current app state asynchronously. + /// - Parameter completion: The completion block called with the app state. + func currentAppState(completion: @escaping (WatchdogTerminationAppState) -> Void) throws { + featureScope.context { context in + let state: WatchdogTerminationAppState = .init( + appVersion: context.version, + osVersion: context.device.osVersion, + systemBootTime: context.device.systemBootTime, + isDebugging: context.device.isDebugging, + wasTerminated: false, + isActive: true, + vendorId: context.device.vendorId, + processId: self.processId, + trackingConsent: context.trackingConsent, + syntheticsEnvironment: self.syntheticsEnvironment + ) + completion(state) + } + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift new file mode 100644 index 0000000000..2318fc59b7 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationChecker.swift @@ -0,0 +1,131 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +#if canImport(UIKit) +import UIKit +#endif + +/// Checks if the app was terminated by Watchdog using heuristics. +/// It uses the app state information from the last app session and the current app session +/// to determine if the app was terminated by Watchdog. +internal final class WatchdogTerminationChecker { + let appStateManager: WatchdogTerminationAppStateManager + let featureScope: FeatureScope + + init( + appStateManager: WatchdogTerminationAppStateManager, + featureScope: FeatureScope + ) { + self.appStateManager = appStateManager + self.featureScope = featureScope + } + + /// Checks if the app was terminated by Watchdog. + /// - Parameters: + /// - launch: The launch report containing information about the app launch. + /// - completion: The completion block called with the result. + func isWatchdogTermination(launch: LaunchReport, completion: @escaping (Bool, WatchdogTerminationAppState?) -> Void) throws { + do { + try appStateManager.currentAppState { current in + self.appStateManager.readAppState { [weak self] previous in + self?.featureScope.context { [weak self] context in + let isWatchdogTermination = self?.isWatchdogTermination(launch: launch, deviceInfo: context.device, from: previous, to: current) + completion(isWatchdogTermination ?? false, previous) + } + } + } + } catch let error { + DD.logger.error("Failed to check if Watchdog Termination occurred", error: error) + completion(false, nil) + throw error + } + } + + /// Checks if the app was terminated by Watchdog. + /// - Parameters: + /// - launch: The launch report containing information about the app launch. + /// - deviceInfo: The device information provided by DatadogContext. + /// - previous: The previous app state stored in the data store from the last app session. + /// - current: The current app state of the app. + func isWatchdogTermination( + launch: LaunchReport, + deviceInfo: DeviceInfo, + from previous: WatchdogTerminationAppState?, + to current: WatchdogTerminationAppState + ) -> Bool { + DD.logger.debug(launch.debugDescription) + DD.logger.debug(previous.debugDescription) + DD.logger.debug(current.debugDescription) + + guard let previous = previous else { + return false + } + + // We can't reliably tell if it was a Watchdog Termination or not if the app was running in a synthetic environment. + // Synthetics uses terminateApp API https://github.com/appium/appium-xcuitest-driver/blob/main/lib/real-device.js#L216 + // for restarting the app which we can't distinguish from Watchdog Termination. + guard previous.syntheticsEnvironment == false else { + return false + } + + // Watchdog Termination detection doesn't work on simulators. + guard deviceInfo.isSimulator == false else { + return false + } + + // When the app is running in debug mode, we can't reliably tell if it was a Watchdog Termination or not. + guard previous.isDebugging == false else { + return false + } + + // Is the app version different than the last time the app was opened? + guard previous.appVersion == current.appVersion else { + return false + } + + // Is there a crash from the last time the app was opened? + guard launch.didCrash == false else { + return false + } + + // Did we receive a termination call the last time the app was opened? + guard previous.wasTerminated == false else { + return false + } + + // Is the OS version different than the last time the app was opened? + guard previous.osVersion == current.osVersion else { + return false + } + + // Was the system rebooted since the last time the app was opened? + guard previous.systemBootTime == current.systemBootTime else { + return false + } + + // This value can change when installing test builds using Xcode or when installing an app + // on a device using ad-hoc distribution. + guard previous.vendorId == current.vendorId else { + return false + } + + // This is likely same process but another check due to stop & start of the SDK + guard previous.processId != current.processId else { + return false + } + + // Was the app in foreground/active ? + // If the app was in background we can't reliably tell if it was a Watchdog Termination or not. + guard previous.isActive else { + return false + } + + return true + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift new file mode 100644 index 0000000000..544a7c2735 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitor.swift @@ -0,0 +1,200 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Monitors the Watchdog Termination events and reports them to Datadog. +internal final class WatchdogTerminationMonitor { + /// The state of the Watchdog Termination Monitor. + enum State { + /// The monitor has started and is listening to the changes. + case started + /// The monitor is starting. It is still not listening to the changes. + case starting + /// The monitor has stopped and is not listening to the changes. + case stopped + } + + enum ErrorMessages { + static let failedToCheckWatchdogTermination = "Failed to check if Watchdog Termination occurred" + static let failedToStartAppState = "Failed to start Watchdog Termination App State Manager" + static let detectedWatchdogTermination = "Based on heuristics, previous app session was terminated by Watchdog" + static let failedToReadViewEvent = "Failed to read the view event from the data store" + static let rumViewEventUpdated = "RUM View event updated" + static let failedToSendWatchdogTermination = "Failed to send Watchdog Termination event" + static let launchTimeFailure = "Failed to send Watchdog Termination event due to not being able to get the launch time" + static let failedToDecodeLaunchReport = "Fails to decode LaunchReport in RUM" + } + + let checker: WatchdogTerminationChecker + let appStateManager: WatchdogTerminationAppStateManager + let feature: FeatureScope + let reporter: WatchdogTerminationReporting + let storage: Storage? + + /// The status of the monitor indicating if it is active or not. + /// When it is active, it listens to the app state changes and updates the app state in the data store. + @ReadWriteLock + internal var currentState: State + + init( + appStateManager: WatchdogTerminationAppStateManager, + checker: WatchdogTerminationChecker, + stroage: Storage?, + feature: FeatureScope, + reporter: WatchdogTerminationReporting + ) { + self.checker = checker + self.appStateManager = appStateManager + self.feature = feature + self.reporter = reporter + self.storage = stroage + self.currentState = .stopped + } + + /// Starts the Watchdog Termination Monitor. + /// - Parameter launchReport: The launch report containing information about the app launch (if available). + func start(launchReport: LaunchReport) { + guard currentState == .stopped else { + return + } + + currentState = .starting + sendWatchTerminationIfFound(launch: launchReport) { [weak self] in + do { + try self?.appStateManager.storeCurrentAppState() + } catch { + DD.logger.error(ErrorMessages.failedToStartAppState, error: error) + self?.feature.telemetry.error(ErrorMessages.failedToStartAppState, error: error) + } + self?.currentState = .started + } + } + + /// Updates the Watchdog Termination Monitor with the given view event. + /// + /// Note: This is a simpler but disk intensive way to store the view event in the data store, + /// because we currently don't offer a way to read the view events from the written batches. + /// This is deliberately done to avoid the complexity of reading the view events from the batches. + /// You can disable Watchdog Terminations tracking by setting `RUM.Configuration.trackWatchdogTerminations` to false. + /// + /// - Parameter viewEvent: The view event which is used to report the Watchdog Termination event. + func update(viewEvent: RUMViewEvent) { + // The monitor state must be started to update the view event, + // because saved view event might be currently used to report the Watchdog Termination event. + guard currentState == .started else { + return + } + + DD.logger.debug(ErrorMessages.rumViewEventUpdated) + feature.rumDataStore.setValue(viewEvent, forKey: .watchdogRUMViewEvent) + } + + /// Checks if the app was terminated by Watchdog and sends the Watchdog Termination event to Datadog. + /// - Parameter launch: The launch report containing information about the app launch. + private func sendWatchTerminationIfFound(launch: LaunchReport, completion: @escaping () -> Void) { + do { + try checker.isWatchdogTermination(launch: launch) { [weak self] isWatchdogTermination, state in + if isWatchdogTermination, let state = state { + DD.logger.debug(ErrorMessages.detectedWatchdogTermination) + self?.sendWatchTermination(state: state, completion: completion) + } else { + completion() + } + } + } catch { + DD.logger.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) + feature.telemetry.error(ErrorMessages.failedToCheckWatchdogTermination, error: error) + completion() + } + } + + /// Sends the Watchdog Termination event to Datadog with the given state. + /// Because Watchdog Termination are reported in the next app session, it uses the saved `RUMViewEvent` + /// to report the event. + /// - Parameter state: The app state when the Watchdog Termination occurred. + private func sendWatchTermination(state: WatchdogTerminationAppState, completion: @escaping () -> Void) { + feature.context { [weak self] context in + guard let launchTime = context.launchTime else { + DD.logger.error(ErrorMessages.launchTimeFailure) + completion() + return + } + + do { + let likelyCrashedAt = try self?.storage?.mostRecentModifiedFileAt(before: launchTime.launchDate) + self?.feature.rumDataStore.value(forKey: .watchdogRUMViewEvent) { [weak self] (viewEvent: RUMViewEvent?) in + guard let viewEvent = viewEvent else { + DD.logger.error(ErrorMessages.failedToReadViewEvent) + self?.feature.telemetry.error(ErrorMessages.failedToReadViewEvent) + completion() + return + } + self?.reporter.send(date: likelyCrashedAt, state: state, viewEvent: viewEvent) + completion() + } + } catch { + DD.logger.error(ErrorMessages.failedToSendWatchdogTermination, error: error) + self?.feature.telemetry.error(ErrorMessages.failedToSendWatchdogTermination, error: error) + completion() + } + } + } + + /// Stops the Watchdog Termination Monitor. + func stop() { + currentState = .stopped + } +} + +extension WatchdogTerminationMonitor: Flushable { + /// Flushes the Watchdog Termination Monitor. It stops the monitor and deletes the app state. + /// - Note: This method must be called manually only or in the tests. + /// This will reset the app state and the monitor will not able to detect Watchdog Termination due to absence of the previous app state. + func flush() { + stop() + } +} + +extension WatchdogTerminationMonitor: FeatureMessageReceiver { + /// Receives the feature message and updates the app state based on the context message. + /// It relies on `ApplicationStatePublisher` context message to update the app state. + /// Other messages are ignored. + /// - Parameters: + /// - message: The feature message. + /// - core: The core instance. + /// - Returns: Always `false`, because it doesn't block the message propagation. + func receive(message: DatadogInternal.FeatureMessage, from core: any DatadogInternal.DatadogCoreProtocol) -> Bool { + feature.context { [weak self] context in + do { + guard let launchReport = try context.baggages[LaunchReport.baggageKey]?.decode(type: LaunchReport.self) else { + return + } + self?.start(launchReport: launchReport) + } catch { + DD.logger.error(ErrorMessages.failedToDecodeLaunchReport, error: error) + self?.feature.telemetry.error(ErrorMessages.failedToDecodeLaunchReport, error: error) + } + } + + // Once the monitor has started, ie watchdog termination check has been done + // we can start updating the app state based on the context message + guard currentState == .started else { + return false + } + + switch message { + case .baggage, .webview, .telemetry: + break + case .context(let context): + let state = context.applicationStateHistory.currentSnapshot.state + appStateManager.updateAppState(state: state) + } + + return false + } +} diff --git a/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift new file mode 100644 index 0000000000..e169874ca5 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/WatchdogTerminations/WatchdogTerminationReporter.swift @@ -0,0 +1,81 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Reports Watchdog Termination events to Datadog. +internal protocol WatchdogTerminationReporting { + /// Sends the Watchdog Termination event to Datadog. + func send(date: Date?, state: WatchdogTerminationAppState, viewEvent: RUMViewEvent) +} + +/// Default implementation of `WatchdogTerminationReporting`. +internal final class WatchdogTerminationReporter: WatchdogTerminationReporting { + enum Constants { + /// The standardized `error.message` for RUM errors describing a Watchdog Termination. + static let errorMessage = "The operating system watchdog terminated the application." + /// The standardized `error.type` for RUM errors describing a Watchdog Termination. + static let errorType = "WatchdogTermination" + /// The standardized `error.stack` when stack trace is not available for Watchdog Termination. + static let stackNotAvailableErrorMessage = "Stack trace is not available for Watchdog Termination." + } + + /// RUM feature scope. + private let featureScope: FeatureScope + + private let dateProvider: DateProvider + + init( + featureScope: FeatureScope, + dateProvider: DateProvider + ) { + self.featureScope = featureScope + self.dateProvider = dateProvider + } + + /// Sends the Watchdog Termination event to Datadog. + func send(date: Date?, state: WatchdogTerminationAppState, viewEvent: RUMViewEvent) { + guard state.trackingConsent == .granted else { // consider the user consent from previous session + DD.logger.debug("Skipped sending Watchdog Termination as it was recorded with \(state.trackingConsent) consent") + return + } + + let errorDate = date ?? Date(timeIntervalSinceReferenceDate: TimeInterval(viewEvent.date)) + + DD.logger.debug("Sending Watchdog Termination event") + featureScope.eventWriteContext(bypassConsent: true) { [dateProvider] context, writer in + let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset) + + let builder = FatalErrorBuilder( + context: context, + error: .watchdogTermination, + errorDate: errorDate, + errorType: Constants.errorType, + errorMessage: Constants.errorMessage, + errorStack: Constants.stackNotAvailableErrorMessage, + errorThreads: nil, + errorBinaryImages: nil, + errorWasTruncated: nil, + errorMeta: nil + ) + let error = builder.createRUMError(with: viewEvent) + let view = builder.updateRUMViewWithError(viewEvent) + + if realDateNow.timeIntervalSince(errorDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold { + DD.logger.debug("Sending Watchdog Termination as RUM error with issuing RUM view update") + // It is still OK to send RUM view to previous RUM session. + writer.write(value: error) + writer.write(value: view) + } else { + // We know it is too late for sending RUM view to previous RUM session as it is now stale on backend. + // To avoid inconsistency, we only send the RUM error. + DD.logger.debug("Sending Watchdog Termination as RUM error without updating RUM view") + writer.write(value: error) + } + } + } +} diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index ffff00b710..a1ece19b6c 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -111,6 +111,13 @@ extension RUM { /// Default: `false`. public var trackBackgroundEvents: Bool + /// Determines whether the SDK should track application termination by the watchdog. + /// + /// Read more about watchdog terminations at https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations + /// + /// Default: `false`. + public var trackWatchdogTerminations: Bool + /// Enables RUM long tasks tracking with the given threshold (in seconds). /// /// Any operation on the main thread that exceeds this threshold will be reported as a RUM long task. @@ -284,6 +291,9 @@ extension RUM { internal var ciTestExecutionID: String? = ProcessInfo.processInfo.environment["CI_VISIBILITY_TEST_EXECUTION_ID"] internal var syntheticsTestId: String? = ProcessInfo.processInfo.environment["_dd.synthetics.test_id"] internal var syntheticsResultId: String? = ProcessInfo.processInfo.environment["_dd.synthetics.result_id"] + internal var syntheticsEnvironment: Bool { + syntheticsTestId != nil || syntheticsResultId != nil + } } } @@ -339,6 +349,7 @@ extension RUM.Configuration { /// - trackBackgroundEvents: Determines whether RUM events should be tracked when no view is active. Default: `false`. /// - longTaskThreshold: The threshold for RUM long tasks tracking (in seconds). Default: `0.1`. /// - appHangThreshold: The threshold for App Hangs monitoring (in seconds). Default: `nil`. + /// - trackWatchdogTerminations: Determines whether the SDK should track application termination by the watchdog. Default: `false`. /// - vitalsUpdateFrequency: The preferred frequency for collecting RUM vitals. Default: `.average`. /// - viewEventMapper: Custom mapper for RUM view events. Default: `nil`. /// - resourceEventMapper: Custom mapper for RUM resource events. Default: `nil`. @@ -358,6 +369,7 @@ extension RUM.Configuration { trackBackgroundEvents: Bool = false, longTaskThreshold: TimeInterval? = 0.1, appHangThreshold: TimeInterval? = nil, + trackWatchdogTerminations: Bool = false, vitalsUpdateFrequency: VitalsFrequency? = .average, viewEventMapper: RUM.ViewEventMapper? = nil, resourceEventMapper: RUM.ResourceEventMapper? = nil, @@ -386,6 +398,7 @@ extension RUM.Configuration { self.onSessionStart = onSessionStart self.customEndpoint = customEndpoint self.telemetrySampleRate = telemetrySampleRate + self.trackWatchdogTerminations = trackWatchdogTerminations } } diff --git a/DatadogRUM/Sources/RUMMonitor/Monitor.swift b/DatadogRUM/Sources/RUMMonitor/Monitor.swift index 4621b81e45..20ae4d8350 100644 --- a/DatadogRUM/Sources/RUMMonitor/Monitor.swift +++ b/DatadogRUM/Sources/RUMMonitor/Monitor.swift @@ -250,7 +250,8 @@ extension Monitor: RUMMonitorProtocol { identity: ViewIdentifier(viewController), name: name ?? viewController.canonicalClassName, path: viewController.canonicalClassName, - attributes: attributes + attributes: attributes, + instrumentationType: .manual ) ) } @@ -272,7 +273,8 @@ extension Monitor: RUMMonitorProtocol { identity: ViewIdentifier(key), name: name ?? key, path: key, - attributes: attributes + attributes: attributes, + instrumentationType: .manual ) ) } diff --git a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift index 8c29297761..b99d863fec 100644 --- a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift +++ b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift @@ -18,6 +18,8 @@ internal protocol RUMCommand { var canStartBackgroundView: Bool { get } /// Whether or not this command is considered a user intaraction var isUserInteraction: Bool { get } + /// A type of event missed upon receiving this command in case of absence of an active view; `nil` if none or N/A. + var missedEventType: SessionEndedMetric.MissedEventType? { get } } internal struct RUMSDKInitCommand: RUMCommand { @@ -25,6 +27,7 @@ internal struct RUMSDKInitCommand: RUMCommand { var attributes: [AttributeKey: AttributeValue] = [:] var canStartBackgroundView = false var isUserInteraction = false + let missedEventType: SessionEndedMetric.MissedEventType? = nil } internal struct RUMApplicationStartCommand: RUMCommand { @@ -32,6 +35,7 @@ internal struct RUMApplicationStartCommand: RUMCommand { var attributes: [AttributeKey: AttributeValue] var canStartBackgroundView = false var isUserInteraction = false + let missedEventType: SessionEndedMetric.MissedEventType? = nil } internal struct RUMStopSessionCommand: RUMCommand { @@ -39,6 +43,7 @@ internal struct RUMStopSessionCommand: RUMCommand { var attributes: [AttributeKey: AttributeValue] = [:] let canStartBackgroundView = false // no, stopping a session should not start a backgorund session let isUserInteraction = false + let missedEventType: SessionEndedMetric.MissedEventType? = nil init(time: Date) { self.time = time @@ -62,18 +67,24 @@ internal struct RUMStartViewCommand: RUMCommand, RUMViewScopePropagatableAttribu /// The path of this View, rendered in RUM Explorer as `VIEW URL`. let path: String + /// The type of instrumentation that started this view. + let instrumentationType: SessionEndedMetric.ViewInstrumentationType + let missedEventType: SessionEndedMetric.MissedEventType? = nil + init( time: Date, identity: ViewIdentifier, name: String, path: String, - attributes: [AttributeKey: AttributeValue] + attributes: [AttributeKey: AttributeValue], + instrumentationType: SessionEndedMetric.ViewInstrumentationType ) { self.time = time self.attributes = attributes self.identity = identity self.name = name self.path = path + self.instrumentationType = instrumentationType } } @@ -85,6 +96,7 @@ internal struct RUMStopViewCommand: RUMCommand, RUMViewScopePropagatableAttribut /// The value holding stable identity of the RUM View. let identity: ViewIdentifier + let missedEventType: SessionEndedMetric.MissedEventType? = nil } /// Any error command, like exception or App Hang. @@ -130,6 +142,7 @@ internal struct RUMAddCurrentViewErrorCommand: RUMErrorCommand { let threads: [DDThread]? let binaryImages: [BinaryImage]? let isStackTraceTruncated: Bool? + let missedEventType: SessionEndedMetric.MissedEventType? = .error /// Constructor dedicated to errors defined by message, type and stack. init( @@ -229,6 +242,7 @@ internal struct RUMAddCurrentViewAppHangCommand: RUMErrorCommand { /// The duration of hang. let hangDuration: TimeInterval + let missedEventType: SessionEndedMetric.MissedEventType? = .error } internal struct RUMAddViewTimingCommand: RUMCommand, RUMViewScopePropagatableAttributes { @@ -240,6 +254,7 @@ internal struct RUMAddViewTimingCommand: RUMCommand, RUMViewScopePropagatableAtt /// The name of the timing. It will be used as a JSON key, whereas the value will be the timing duration, /// measured since the start of the View. let timingName: String + let missedEventType: SessionEndedMetric.MissedEventType? = nil } // MARK: - RUM Resource related commands @@ -275,6 +290,7 @@ internal struct RUMStartResourceCommand: RUMResourceCommand { let kind: RUMResourceType? /// Span context passed to the RUM backend in order to generate the APM span for underlying resource. let spanContext: RUMSpanContext? + let missedEventType: SessionEndedMetric.MissedEventType? = .resource } internal struct RUMAddResourceMetricsCommand: RUMResourceCommand { @@ -286,6 +302,7 @@ internal struct RUMAddResourceMetricsCommand: RUMResourceCommand { /// Resource metrics. let metrics: ResourceMetrics + let missedEventType: SessionEndedMetric.MissedEventType? = nil } internal struct RUMStopResourceCommand: RUMResourceCommand { @@ -301,6 +318,7 @@ internal struct RUMStopResourceCommand: RUMResourceCommand { let httpStatusCode: Int? /// The size of loaded Resource let size: Int64? + let missedEventType: SessionEndedMetric.MissedEventType? = nil } internal struct RUMStopResourceWithErrorCommand: RUMResourceCommand { @@ -322,6 +340,7 @@ internal struct RUMStopResourceWithErrorCommand: RUMResourceCommand { let stack: String? /// HTTP status code of the Ressource error. let httpStatusCode: Int? + let missedEventType: SessionEndedMetric.MissedEventType? = .error init( resourceKey: String, @@ -385,6 +404,7 @@ internal struct RUMStartUserActionCommand: RUMUserActionCommand { let actionType: RUMActionType let name: String + let missedEventType: SessionEndedMetric.MissedEventType? = .action } /// Stops continuous User Action. @@ -396,6 +416,7 @@ internal struct RUMStopUserActionCommand: RUMUserActionCommand { let actionType: RUMActionType let name: String? + let missedEventType: SessionEndedMetric.MissedEventType? = nil } /// Adds discrete (discontinuous) User Action. @@ -407,6 +428,7 @@ internal struct RUMAddUserActionCommand: RUMUserActionCommand { let actionType: RUMActionType let name: String + let missedEventType: SessionEndedMetric.MissedEventType? = .action } /// Adds that a feature flag has been evaluated to the view @@ -417,6 +439,7 @@ internal struct RUMAddFeatureFlagEvaluationCommand: RUMCommand { let isUserInteraction = false let name: String let value: Encodable + let missedEventType: SessionEndedMetric.MissedEventType? = nil init(time: Date, name: String, value: Encodable) { self.time = time @@ -435,6 +458,7 @@ internal struct RUMAddLongTaskCommand: RUMCommand { let isUserInteraction = false // a long task is not an interactive event let duration: TimeInterval + let missedEventType: SessionEndedMetric.MissedEventType? = .longTask } // MARK: - RUM Web Events related commands @@ -445,6 +469,7 @@ internal struct RUMKeepSessionAliveCommand: RUMCommand { let isUserInteraction = false var time: Date var attributes: [AttributeKey: AttributeValue] + let missedEventType: SessionEndedMetric.MissedEventType? = nil } // MARK: - Cross-platform perf metrics @@ -456,4 +481,5 @@ internal struct RUMUpdatePerformanceMetric: RUMCommand { let value: Double var time: Date var attributes: [AttributeKey: AttributeValue] + let missedEventType: SessionEndedMetric.MissedEventType? = nil } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift index 305ef959d2..979522394a 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -102,7 +102,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // proccss(command:context:writer) returned false, so the scope will be deallocated at the end of // this execution context. End the "RUM Session Ended" metric: - defer { dependencies.sessionEndedMetric.endMetric(sessionID: scope.sessionUUID) } + defer { dependencies.sessionEndedMetric.endMetric(sessionID: scope.sessionUUID, with: context) } // proccss(command:context:writer) returned false, but if the scope is still active // it means the session reached one of the end reasons diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift index b40734c1f1..807b735908 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift @@ -49,6 +49,7 @@ internal struct RUMScopeDependencies { let telemetry: Telemetry let sessionType: RUMSessionType let sessionEndedMetric: SessionEndedMetricController + let watchdogTermination: WatchdogTerminationMonitor? init( featureScope: FeatureScope, @@ -66,7 +67,8 @@ internal struct RUMScopeDependencies { onSessionStart: RUM.SessionListener?, viewCache: ViewCache, fatalErrorContext: FatalErrorContextNotifying, - sessionEndedMetric: SessionEndedMetricController + sessionEndedMetric: SessionEndedMetricController, + watchdogTermination: WatchdogTerminationMonitor? ) { self.featureScope = featureScope self.rumApplicationID = rumApplicationID @@ -85,6 +87,7 @@ internal struct RUMScopeDependencies { self.fatalErrorContext = fatalErrorContext self.telemetry = featureScope.telemetry self.sessionEndedMetric = sessionEndedMetric + self.watchdogTermination = watchdogTermination if ciTest != nil { self.sessionType = .ciTest diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift index cee8ae1a75..0bd272a18b 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift @@ -108,7 +108,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { dependencies.sessionEndedMetric.startMetric( sessionID: sessionUUID, precondition: startPrecondition, - context: context + context: context, + tracksBackgroundEvents: trackBackgroundEvents ) if let viewScope = resumingViewScope { @@ -321,13 +322,18 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { case .handleInBackgroundView where command.canStartBackgroundView: startBackgroundView(on: command, context: context) default: + if let missedEventType = command.missedEventType { + // In case there was an event missed due to no active view, track it in Session Ended metric + dependencies.sessionEndedMetric.track(missedEventType: missedEventType, in: sessionUUID) + } + if !(command is RUMKeepSessionAliveCommand) { // it is expected to receive 'keep alive' while no active view (when tracking WebView events) // As no view scope will handle this command, warn the user on dropping it. DD.logger.warn( """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. + \(String(describing: command)) was detected, but no view is active. To track views automatically, configure + `RUM.Configuration.uiKitViewsPredicate` or use `.trackRUMView()` modifier in SwiftUI. You can also track views manually + with `RUMMonitor.shared().startView()` and `RUMMonitor.shared().stopView()`. """ ) } diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift index cf30cce593..5eb1de30d3 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMViewScope.swift @@ -556,7 +556,16 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { dependencies.fatalErrorContext.view = event // Track this view in Session Ended metric: - dependencies.sessionEndedMetric.track(view: event, in: self.context.sessionID) + dependencies.sessionEndedMetric.track( + view: event, + instrumentationType: (command as? RUMStartViewCommand)?.instrumentationType, + in: self.context.sessionID + ) + + // Update the state of the view in watchdog termination monitor + // if a watchdog termination occurs in this session, in the next session + // a watchdog termination event will be sent using saved view event. + dependencies.watchdogTermination?.update(viewEvent: event) } else { // if event was dropped by mapper version -= 1 } diff --git a/DatadogRUM/Sources/RUMMonitorProtocol+Convenience.swift b/DatadogRUM/Sources/RUMMonitorProtocol+Convenience.swift index f16dc605d4..bf8b846b92 100644 --- a/DatadogRUM/Sources/RUMMonitorProtocol+Convenience.swift +++ b/DatadogRUM/Sources/RUMMonitorProtocol+Convenience.swift @@ -77,7 +77,7 @@ public extension RUMMonitorProtocol { /// - stack: stack trace of the error. No specific format is required. If not specified, it will be inferred from `file` and `line`. /// - source: the origin of the error. /// - attributes: custom attributes to attach to this error. - /// - file: the file in which the error occurred (the default is the `#filePath` of the caller). + /// - file: the file in which the error occurred (the default is the `#fileID` of the caller). /// - line: the line number on which the error occurred (the default is the `#line` of the caller). func addError( message: String, @@ -85,7 +85,7 @@ public extension RUMMonitorProtocol { stack: String? = nil, source: RUMErrorSource = .custom, attributes: [AttributeKey: AttributeValue] = [:], - file: StaticString? = #filePath, + file: StaticString? = #fileID, line: UInt? = #line ) { addError( diff --git a/DatadogRUM/Sources/RUMMonitorProtocol.swift b/DatadogRUM/Sources/RUMMonitorProtocol.swift index b56235f44b..2193938ca2 100644 --- a/DatadogRUM/Sources/RUMMonitorProtocol.swift +++ b/DatadogRUM/Sources/RUMMonitorProtocol.swift @@ -126,7 +126,7 @@ public protocol RUMMonitorProtocol: AnyObject { /// - stack: stack trace of the error. No specific format is required. If not specified, it will be inferred from `file` and `line`. /// - source: the origin of the error. /// - attributes: custom attributes to attach to this error. - /// - file: the file in which the error occurred (the default is the `#filePath` of the caller). + /// - file: the file in which the error occurred (the default is the `#fileID` of the caller). /// - line: the line number on which the error occurred (the default is the `#line` of the caller). func addError( message: String, diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift index 3d417d7c9e..989adf5f5e 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetric.swift @@ -23,7 +23,11 @@ internal enum SessionEndedMetricError: Error, CustomStringConvertible { } /// Tracks the state of RUM session and exports attributes for "RUM Session Ended" telemetry. -internal struct SessionEndedMetric { +/// +/// It is modeled as a reference type and contains mutable state. The thread safety for its mutations is +/// achieved by design: only `SessionEndedMetricController` interacts with this class and does +/// it through critical section each time. +internal class SessionEndedMetric { /// Definition of fields in "RUM Session Ended" telemetry, following the "RUM Session Ended" telemetry spec. internal enum Constants { /// The name of this metric, included in telemetry log. @@ -35,6 +39,16 @@ internal struct SessionEndedMetric { static let rseKey = "rse" } + /// Represents the type of instrumentation used to start a view. + internal enum ViewInstrumentationType: String, Encodable { + /// View was started manually through `RUMMonitor.shared().startView()` API. + case manual + /// View was started automatically with `UIKitRUMViewsPredicate`. + case uikit + /// View was started through `trackRUMView()` SwiftUI modifier. + case swiftui + } + /// An ID of the session being tracked through this metric object. let sessionID: RUMUUID @@ -44,16 +58,36 @@ internal struct SessionEndedMetric { /// The session precondition that led to the creation of this session. private let precondition: RUMSessionPrecondition? - private struct TrackedViewInfo { + /// Tracks view information for certain `view.id`. + private class TrackedViewInfo { + /// The view URL as reported in RUM data. let viewURL: String + /// The type of instrumentation that started this view. + /// It can be `nil` if view was started implicitly by RUM, which is the case for "ApplicationLaunch" and "Background" views. + let instrumentationType: ViewInstrumentationType? + /// The start of the view in milliseconds from from epoch. let startMs: Int64 + /// The duration of the view in nanoseconds. var durationNs: Int64 - - // TODO: RUM-4591 Track diagnostic attributes: - // - `instrumentationType`: manual | uikit | swiftui + /// If any of view updates to this `view.id` had `session.has_replay == true`. + var hasReplay: Bool + + init( + viewURL: String, + instrumentationType: ViewInstrumentationType?, + startMs: Int64, + durationNs: Int64, + hasReplay: Bool + ) { + self.viewURL = viewURL + self.instrumentationType = instrumentationType + self.startMs = startMs + self.durationNs = durationNs + self.hasReplay = hasReplay + } } - /// Stores information about tracked views, referencing them by their view ID. + /// Stores information about tracked views, referencing them by their `view.id`. private var trackedViews: [String: TrackedViewInfo] = [:] /// Info about the first tracked view. @@ -68,11 +102,22 @@ internal struct SessionEndedMetric { /// Indicates if the session was stopped through `stopSession()` API. private var wasStopped = false - // TODO: RUM-4591 Track diagnostic attributes: - // - no_view_events_count - // - has_background_events_tracking_enabled - // - has_replay - // - ntp_offset + /// If `RUM.Configuration.trackBackgroundEvents` was enabled for this session. + private let tracksBackgroundEvents: Bool + + /// The current value of NTP offset at session start. + private let ntpOffsetAtStart: TimeInterval + + /// Represents types of event that can be missed due to absence of an active RUM view. + enum MissedEventType: String { + case action + case resource + case error + case longTask + } + + /// Tracks the number of RUM events missed due to absence of an active RUM view. + private var missedEvents: [MissedEventType: Int] = [:] // MARK: - Tracking Metric State @@ -81,30 +126,46 @@ internal struct SessionEndedMetric { /// - sessionID: An ID of the session that is being tracked with this metric. /// - precondition: The precondition that led to starting this session. /// - context: The SDK context at the moment of starting this session. + /// - tracksBackgroundEvents: If background events tracking is enabled for this session. init( sessionID: RUMUUID, precondition: RUMSessionPrecondition?, - context: DatadogContext + context: DatadogContext, + tracksBackgroundEvents: Bool ) { self.sessionID = sessionID self.bundleType = context.applicationBundleType self.precondition = precondition + self.tracksBackgroundEvents = tracksBackgroundEvents + self.ntpOffsetAtStart = context.serverTimeOffset } /// Tracks the view event that occurred during the session. - mutating func track(view: RUMViewEvent) throws { + /// - Parameters: + /// - view: the view event to track + /// - instrumentationType: the type of instrumentation used to start this view (only the first value for each `view.id` is tracked; succeeding values + /// will be ignored so it is okay to pass value on first call and then follow with `nil` for next updates of given `view.id`) + func track(view: RUMViewEvent, instrumentationType: ViewInstrumentationType?) throws { guard view.session.id == sessionID.toRUMDataFormat else { throw SessionEndedMetricError.trackingViewInForeignSession(viewURL: view.view.url, sessionID: sessionID) } - var info = trackedViews[view.view.id] ?? TrackedViewInfo( - viewURL: view.view.url, - startMs: view.date, - durationNs: view.view.timeSpent - ) + let info: TrackedViewInfo - info.durationNs = view.view.timeSpent - trackedViews[view.view.id] = info + if let existingInfo = trackedViews[view.view.id] { + info = existingInfo + info.durationNs = view.view.timeSpent + info.hasReplay = info.hasReplay || (view.session.hasReplay ?? false) + } else { + info = TrackedViewInfo( + viewURL: view.view.url, + instrumentationType: instrumentationType, + startMs: view.date, + durationNs: view.view.timeSpent, + hasReplay: view.session.hasReplay ?? false + ) + trackedViews[view.view.id] = info + } if firstTrackedView == nil { firstTrackedView = info @@ -113,7 +174,8 @@ internal struct SessionEndedMetric { } /// Tracks the kind of SDK error that occurred during the session. - mutating func track(sdkErrorKind: String) { + /// - Parameter sdkErrorKind: the kind of SDK error + func track(sdkErrorKind: String) { if let count = trackedSDKErrors[sdkErrorKind] { trackedSDKErrors[sdkErrorKind] = count + 1 } else { @@ -121,8 +183,18 @@ internal struct SessionEndedMetric { } } + /// Tracks an event missed due to absence of an active view. + /// - Parameter missedEventType: the type of an event that was missed + func track(missedEventType: MissedEventType) { + if let count = missedEvents[missedEventType] { + missedEvents[missedEventType] = count + 1 + } else { + missedEvents[missedEventType] = 1 + } + } + /// Signals that the session was stopped with `stopSession()` API. - mutating func trackWasStopped() { + func trackWasStopped() { wasStopped = true } @@ -145,6 +217,8 @@ internal struct SessionEndedMetric { let duration: Int64? /// Indicates if the session was stopped through `stopSession()` API. let wasStopped: Bool + /// If background events tracking is enabled for this session. + let hasBackgroundEventsTrackingEnabled: Bool struct ViewsCount: Encodable { /// The number of distinct views (view UUIDs) sent during this session. @@ -153,11 +227,17 @@ internal struct SessionEndedMetric { let background: Int /// The number of standard "ApplicationLaunch" views tracked during this session (sanity check: we expect `0` or `1`). let applicationLaunch: Int + /// The map of view instrumentation types to the number of views tracked with each instrumentation. + let byInstrumentation: [String: Int] + /// The number of distinct views that had `has_replay == true` in any of their view events. + let withHasReplay: Int enum CodingKeys: String, CodingKey { case total case background case applicationLaunch = "app_launch" + case byInstrumentation = "by_instrumentation" + case withHasReplay = "with_has_replay" } } @@ -179,18 +259,61 @@ internal struct SessionEndedMetric { let sdkErrorsCount: SDKErrorsCount + struct NTPOffset: Encodable { + /// The NTP offset at session start, in milliseconds. + let atStart: Int64 + /// The NTP offset at session end, in milliseconds. + let atEnd: Int64 + + enum CodingKeys: String, CodingKey { + case atStart = "at_start" + case atEnd = "at_end" + } + } + + /// NTP offset information tracked for this session. + let ntpOffset: NTPOffset + + struct NoViewEventsCount: Encodable { + /// Number of action events missed due to absence of an active view. + let actions: Int + /// Number of resource events missed due to absence of an active view. + let resources: Int + /// Number of error events missed due to absence of an active view. + let errors: Int + /// Number of long task events missed due to absence of an active view. + let longTasks: Int + + enum CodingKeys: String, CodingKey { + case actions + case resources + case errors + case longTasks = "long_tasks" + } + } + + /// Information on number of events missed due to absence of an active view. + let noViewEventsCount: NoViewEventsCount + enum CodingKeys: String, CodingKey { case processType = "process_type" case precondition case duration case wasStopped = "was_stopped" + case hasBackgroundEventsTrackingEnabled = "has_background_events_tracking_enabled" case viewsCount = "views_count" case sdkErrorsCount = "sdk_errors_count" + case ntpOffset = "ntp_offset" + case noViewEventsCount = "no_view_events_count" } } - /// Exports metric attributes for `Telemetry.metric(name:attributes:)`. - func asMetricAttributes() -> [String: Encodable] { + /// Exports metric attributes for `Telemetry.metric(name:attributes:)`. This method is expected to be called + /// at session end with providing the SDK `context` valid at the moment of call. + /// + /// - Parameter context: the SDK context valid at the moment of this call + /// - Returns: metric attributes + func asMetricAttributes(with context: DatadogContext) -> [String: Encodable] { // Compute duration var durationNs: Int64? if let firstView = firstTrackedView, let lastView = lastTrackedView { @@ -202,6 +325,13 @@ internal struct SessionEndedMetric { let totalViewsCount = trackedViews.count let backgroundViewsCount = trackedViews.values.filter({ $0.viewURL == RUMOffViewEventsHandlingRule.Constants.backgroundViewURL }).count let appLaunchViewsCount = trackedViews.values.filter({ $0.viewURL == RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL }).count + var byInstrumentationViewsCount: [String: Int] = [:] + trackedViews.values.forEach { + if let instrumentationType = $0.instrumentationType?.rawValue { + byInstrumentationViewsCount[instrumentationType] = (byInstrumentationViewsCount[instrumentationType] ?? 0) + 1 + } + } + let withHasReplayCount = trackedViews.values.reduce(0, { acc, next in acc + (next.hasReplay ? 1 : 0) }) // Compute SDK errors count let totalSDKErrors = trackedSDKErrors.values.reduce(0, +) @@ -220,14 +350,27 @@ internal struct SessionEndedMetric { precondition: precondition?.rawValue, duration: durationNs, wasStopped: wasStopped, + hasBackgroundEventsTrackingEnabled: tracksBackgroundEvents, viewsCount: .init( total: totalViewsCount, background: backgroundViewsCount, - applicationLaunch: appLaunchViewsCount + applicationLaunch: appLaunchViewsCount, + byInstrumentation: byInstrumentationViewsCount, + withHasReplay: withHasReplayCount ), sdkErrorsCount: .init( total: totalSDKErrors, byKind: top5SDKErrorsByKind + ), + ntpOffset: .init( + atStart: ntpOffsetAtStart.toInt64Milliseconds, + atEnd: context.serverTimeOffset.toInt64Milliseconds + ), + noViewEventsCount: .init( + actions: missedEvents[.action] ?? 0, + resources: missedEvents[.resource] ?? 0, + errors: missedEvents[.error] ?? 0, + longTasks: missedEvents[.longTask] ?? 0 ) ) ] diff --git a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift index 6a1ca81354..797e93ac44 100644 --- a/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift +++ b/DatadogRUM/Sources/SDKMetrics/SessionEndedMetricController.swift @@ -29,13 +29,14 @@ internal final class SessionEndedMetricController { /// - sessionID: The ID of the session to track. /// - precondition: The precondition that led to starting this session. /// - context: The SDK context at the moment of starting this session. + /// - tracksBackgroundEvents: If background events tracking is enabled for this session. /// - Returns: The newly created `SessionEndedMetric` instance. - func startMetric(sessionID: RUMUUID, precondition: RUMSessionPrecondition?, context: DatadogContext) { + func startMetric(sessionID: RUMUUID, precondition: RUMSessionPrecondition?, context: DatadogContext, tracksBackgroundEvents: Bool) { guard sessionID != RUMUUID.nullUUID else { return // do not track metric when session is not sampled } _metricsBySessionID.mutate { metrics in - metrics[sessionID] = SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context) + metrics[sessionID] = SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context, tracksBackgroundEvents: tracksBackgroundEvents) pendingSessionIDs.append(sessionID) } } @@ -43,9 +44,15 @@ internal final class SessionEndedMetricController { /// Tracks the view event that occurred during the session. /// - Parameters: /// - view: the view event to track + /// - instrumentationType: the type of instrumentation used to start this view (only the first value for each `view.id` is tracked; succeeding values + /// will be ignored so it is okay to pass value on first call and then follow with `nil` for next updates of given `view.id`) /// - sessionID: session ID to track this view in (pass `nil` to track it for the last started session) - func track(view: RUMViewEvent, in sessionID: RUMUUID?) { - updateMetric(for: sessionID) { try $0?.track(view: view) } + func track( + view: RUMViewEvent, + instrumentationType: SessionEndedMetric.ViewInstrumentationType?, + in sessionID: RUMUUID? + ) { + updateMetric(for: sessionID) { try $0?.track(view: view, instrumentationType: instrumentationType) } } /// Tracks the kind of SDK error that occurred during the session. @@ -56,6 +63,14 @@ internal final class SessionEndedMetricController { updateMetric(for: sessionID) { $0?.track(sdkErrorKind: sdkErrorKind) } } + /// Tracks an event missed due to absence of an active view. + /// - Parameters: + /// - missedEventType: the type of an event that was missed + /// - sessionID: session ID to track this error in (pass `nil` to track it for the last started session) + func track(missedEventType: SessionEndedMetric.MissedEventType, in sessionID: RUMUUID?) { + updateMetric(for: sessionID) { $0?.track(missedEventType: missedEventType) } + } + /// Signals that the session was stopped with `stopSession()` API. /// - Parameter sessionID: session ID to mark as stopped (pass `nil` to track it for the last started session) func trackWasStopped(sessionID: RUMUUID?) { @@ -64,12 +79,12 @@ internal final class SessionEndedMetricController { /// Ends the metric for a given session, sending it to telemetry and removing it from pending metrics. /// - Parameter sessionID: The ID of the session to end the metric for. - func endMetric(sessionID: RUMUUID) { - guard let metric = metricsBySessionID[sessionID] else { - return - } - telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes()) + func endMetric(sessionID: RUMUUID, with context: DatadogContext) { _metricsBySessionID.mutate { metrics in + guard let metric = metrics[sessionID] else { + return + } + telemetry.metric(name: SessionEndedMetric.Constants.name, attributes: metric.asMetricAttributes(with: context)) metrics[sessionID] = nil pendingSessionIDs.removeAll(where: { $0 == sessionID }) // O(n), but "ending the metric" is very rare event } diff --git a/DatadogRUM/Tests/DDTAssertValidRUMUUID.swift b/DatadogRUM/Tests/DDTAssertValidRUMUUID.swift index ad76e14cc9..8b083437b8 100644 --- a/DatadogRUM/Tests/DDTAssertValidRUMUUID.swift +++ b/DatadogRUM/Tests/DDTAssertValidRUMUUID.swift @@ -7,7 +7,7 @@ import Foundation import TestUtilities -func DDTAssertValidRUMUUID(_ uuid: @autoclosure () throws -> String?, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { +func DDTAssertValidRUMUUID(_ uuid: @autoclosure () throws -> String?, _ message: @autoclosure () -> String = "", file: StaticString = #fileID, line: UInt = #line) { _DDEvaluateAssertion(message: message(), file: file, line: line) { try _DDTAssertValidRUMUUID(uuid()) } diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 9cb84d1fb9..3f404d6bab 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -24,7 +24,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -49,7 +50,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -71,7 +73,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -96,7 +99,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -117,7 +121,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -138,7 +143,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) // Then @@ -159,7 +165,8 @@ class RUMInstrumentationTests: XCTestCase { dateProvider: SystemDateProvider(), backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), - processID: .mockAny() + processID: .mockAny(), + watchdogTermination: .mockRandom() ) let subscriber = RUMCommandSubscriberMock() @@ -176,7 +183,7 @@ class RUMInstrumentationTests: XCTestCase { } } -internal func DDAssertActiveSwizzlings(_ expectedSwizzledSelectors: [String], file: StaticString = #filePath, line: UInt = #line) { +internal func DDAssertActiveSwizzlings(_ expectedSwizzledSelectors: [String], file: StaticString = #fileID, line: UInt = #line) { _DDEvaluateAssertion(message: "Only \(expectedSwizzledSelectors) swizzlings should be active", file: file, line: line) { let actual = Swizzling.methods.map { "\(method_getName($0))" }.sorted() let expected = expectedSwizzledSelectors.sorted() diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift new file mode 100644 index 0000000000..c529fec00b --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationAppStateManagerTests.swift @@ -0,0 +1,73 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationAppStateManagerTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + var sut: WatchdogTerminationAppStateManager! + var featureScope: FeatureScopeMock! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + + featureScope = FeatureScopeMock() + sut = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: .init(), + syntheticsEnvironment: false + ) + } + + func testUpdateAppState_SetsIsActive() throws { + try sut.storeCurrentAppState() + + let isActiveExpectation = expectation(description: "isActive is set to true") + + // app state changes + sut.updateAppState(state: .active) + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertTrue(appState?.isActive == true) + isActiveExpectation.fulfill() + } + wait(for: [isActiveExpectation], timeout: 1) + + let isBackgroundedExpectation = expectation(description: "isActive is set to false") + + // app state changes again + sut.updateAppState(state: .background) + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertTrue(appState?.isActive == false) + isBackgroundedExpectation.fulfill() + } + + wait(for: [isBackgroundedExpectation], timeout: 1) + } + + func testDeleteAppState() throws { + try sut.storeCurrentAppState() + + let isActiveExpectation = expectation(description: "isActive is set") + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertNotNil(appState) + isActiveExpectation.fulfill() + } + wait(for: [isActiveExpectation], timeout: 1) + + let deleteExpectation = expectation(description: "isActive is set to false") + sut.deleteAppState() + featureScope.rumDataStore.value(forKey: .watchdogAppStateKey) { (appState: WatchdogTerminationAppState?) in + XCTAssertNil(appState) + deleteExpectation.fulfill() + } + + wait(for: [deleteExpectation], timeout: 1) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift new file mode 100644 index 0000000000..84e3b01455 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationCheckerTests.swift @@ -0,0 +1,326 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationCheckerTests: XCTestCase { + var sut: WatchdogTerminationChecker = .init(appStateManager: .mockRandom(), featureScope: FeatureScopeMock()) + + func testNoPreviousState_NoWatchdogTermination() throws { + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), deviceInfo: .mockWith(isSimulator: false), from: nil, to: .mockRandom())) + } + + func testSyntheticsEnvironment_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: true, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: true + ) + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), deviceInfo: .mockWith(isSimulator: false), from: previous, to: .mockRandom())) + } + + func testIsSimulatorBuild_NoWatchdogTermination() throws { + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), deviceInfo: .mockWith(isSimulator: false), from: .mockRandom(), to: .mockRandom())) + } + + func testIsDebugging_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: true, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), deviceInfo: .mockWith(isSimulator: false), from: previous, to: .mockRandom())) + } + + func testDifferentAppVersions_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.1", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .mockRandom(), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testApplicationDidCrash_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: true), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testApplicationWasTerminated_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: true, + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testDifferentOSVersions_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.1", + systemBootTime: .mockAny(), + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testDifferentBootTimes_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 2.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testDifferentVendorId_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "bar", + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testSDKWasStoppedAndStarted_NoWatchdogTermination() throws { + let pid = UUID() + + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: pid, + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: pid, + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testApplicationWasInBackground_NoWatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: false, + vendorId: "foo", + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: "foo", + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertFalse(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } + + func testApplicationWasInForeground_WatchdogTermination() throws { + let previous = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: true, + vendorId: "foo", + processId: UUID(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + let current = WatchdogTerminationAppState( + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + isDebugging: false, + wasTerminated: .mockAny(), + isActive: true, + vendorId: "foo", + processId: UUID(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: false + ) + + XCTAssertTrue(sut.isWatchdogTermination(launch: .init(didCrash: false), deviceInfo: .mockWith(isSimulator: false), from: previous, to: current)) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift new file mode 100644 index 0000000000..30acf3c4e6 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMocks.swift @@ -0,0 +1,117 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +extension WatchdogTerminationAppState: RandomMockable, AnyMockable { + public static func mockAny() -> DatadogRUM.WatchdogTerminationAppState { + return .init( + appVersion: .mockAny(), + osVersion: .mockAny(), + systemBootTime: .mockAny(), + isDebugging: .mockAny(), + wasTerminated: .mockAny(), + isActive: .mockAny(), + vendorId: .mockAny(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: .mockRandom() + ) + } + + public static func mockRandom() -> WatchdogTerminationAppState { + return .init( + appVersion: .mockRandom(), + osVersion: .mockRandom(), + systemBootTime: .mockRandom(), + isDebugging: .mockRandom(), + wasTerminated: .mockRandom(), + isActive: .mockRandom(), + vendorId: .mockRandom(), + processId: .mockAny(), + trackingConsent: .mockRandom(), + syntheticsEnvironment: .mockRandom() + ) + } +} + +class WatchdogTerminationReporterMock: WatchdogTerminationReporting { + var didSend: XCTestExpectation + var sendParams: SendParams? + + init(didSend: XCTestExpectation) { + self.didSend = didSend + } + + func send(date: Date?, state: DatadogRUM.WatchdogTerminationAppState, viewEvent: DatadogRUM.RUMViewEvent) { + sendParams = SendParams(date: date, state: state, viewEvent: viewEvent) + didSend.fulfill() + } + + struct SendParams { + let date: Date? + let state: DatadogRUM.WatchdogTerminationAppState + let viewEvent: DatadogRUM.RUMViewEvent + } +} + +extension WatchdogTerminationReporter: RandomMockable { + public static func mockRandom() -> Self { + return .init(featureScope: FeatureScopeMock(), dateProvider: DateProviderMock()) + } +} + +extension WatchdogTerminationChecker: RandomMockable { + public static func mockRandom() -> WatchdogTerminationChecker { + return .init( + appStateManager: .mockRandom(), + featureScope: FeatureScopeMock() + ) + } +} + +extension WatchdogTerminationAppStateManager: RandomMockable { + public static func mockRandom() -> WatchdogTerminationAppStateManager { + return .init( + featureScope: FeatureScopeMock(), + processId: .mockRandom(), + syntheticsEnvironment: .mockRandom() + ) + } +} + +extension Sysctl: RandomMockable { + public static func mockRandom() -> DatadogInternal.Sysctl { + return .init() + } +} + +extension RUMDataStore: RandomMockable { + public static func mockRandom() -> DatadogRUM.RUMDataStore { + return .init(featureScope: FeatureScopeMock()) + } +} + +extension WatchdogTerminationMonitor: RandomMockable { + public static func mockRandom() -> WatchdogTerminationMonitor { + return .init( + appStateManager: .mockRandom(), + checker: .mockRandom(), + stroage: NOPDatadogCore().storage, + feature: FeatureScopeMock(), + reporter: WatchdogTerminationReporter.mockRandom() + ) + } +} + +extension LaunchReport: RandomMockable { + public static func mockRandom() -> DatadogInternal.LaunchReport { + return .init(didCrash: .mockRandom()) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift new file mode 100644 index 0000000000..c55a0689ff --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationMonitorTests.swift @@ -0,0 +1,119 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationMonitorTests: XCTestCase { + let featureScope = FeatureScopeMock() + + // swiftlint:disable implicitly_unwrapped_optional + var sut: WatchdogTerminationMonitor! + var reporter: WatchdogTerminationReporterMock! + // swiftlint:enable implicitly_unwrapped_optional + + func testApplicationWasInForeground_WatchdogTermination() throws { + let didSend = self.expectation(description: "Watchdog termination was reported") + + // app starts + given( + isSimulator: false, + isDebugging: false, + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + vendorId: "foo", + processId: UUID(), + didCrash: false, + didSend: didSend + ) + + // RUM view update before start, this must be ignored + let viewEvent1: RUMViewEvent = .mockRandom() + sut.update(viewEvent: viewEvent1) + + // monitor reveives the launch report + _ = sut.receive(message: .context(.mockAny()), from: NOPDatadogCore()) + + // RUM view update after start + let viewEvent2: RUMViewEvent = .mockRandom() + sut.update(viewEvent: viewEvent2) + + // watchdog termination happens here which causes app launch + given( + isSimulator: false, + isDebugging: false, + appVersion: "1.0.0", + osVersion: "1.0.0", + systemBootTime: 1.0, + vendorId: "foo", + processId: UUID(), + didCrash: false, + didSend: didSend + ) + + // RUM view update before start, this must be ignored + let viewEvent3: RUMViewEvent = .mockRandom() + sut.update(viewEvent: viewEvent3) + + // monitor reveives the launch report + _ = sut.receive(message: .context(.mockAny()), from: NOPDatadogCore()) + + waitForExpectations(timeout: 1) + XCTAssertEqual(reporter.sendParams?.viewEvent.view.id, viewEvent2.view.id) + } + + // MARK: Helpers + + func given( + isSimulator: Bool, + isDebugging: Bool, + appVersion: String, + osVersion: String, + systemBootTime: TimeInterval, + vendorId: String?, + processId: UUID, + didCrash: Bool, + didSend: XCTestExpectation + ) { + let deviceInfo: DeviceInfo = .init( + name: .mockAny(), + model: .mockAny(), + osName: .mockAny(), + osVersion: .mockAny(), + osBuildNumber: .mockAny(), + architecture: .mockAny(), + isSimulator: isSimulator, + vendorId: vendorId, + isDebugging: false, + systemBootTime: systemBootTime + ) + + featureScope.contextMock.version = appVersion + featureScope.contextMock.device = deviceInfo + featureScope.contextMock.baggages[LaunchReport.baggageKey] = .init(LaunchReport(didCrash: didCrash)) + + let appStateManager = WatchdogTerminationAppStateManager( + featureScope: featureScope, + processId: processId, + syntheticsEnvironment: false + ) + + let checker = WatchdogTerminationChecker(appStateManager: appStateManager, featureScope: featureScope) + + reporter = WatchdogTerminationReporterMock(didSend: didSend) + + sut = WatchdogTerminationMonitor( + appStateManager: appStateManager, + checker: checker, + stroage: NOPDatadogCore().storage, + feature: featureScope, + reporter: reporter + ) + } +} diff --git a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift index 0fe552c2c1..deb686f5ec 100644 --- a/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift +++ b/DatadogRUM/Tests/Integrations/TelemetryInterceptorTests.swift @@ -20,13 +20,13 @@ class TelemetryInterceptorTests: XCTestCase { let interceptor = TelemetryInterceptor(sessionEndedMetric: metricController) // When - metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny()) + metricController.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockAny(), tracksBackgroundEvents: .mockRandom()) let errorTelemetry: TelemetryMessage = .error(id: .mockAny(), message: .mockAny(), kind: .mockAny(), stack: .mockAny()) let result = interceptor.receive(message: .telemetry(errorTelemetry), from: NOPDatadogCore()) XCTAssertFalse(result) // Then - metricController.endMetric(sessionID: sessionID) + metricController.endMetric(sessionID: sessionID, with: .mockRandom()) let metric = try XCTUnwrap(telemetry.messages.lastMetric(named: SessionEndedMetric.Constants.name)) let rse = try XCTUnwrap(metric.attributes[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes) XCTAssertEqual(rse.sdkErrorsCount.total, 1) diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index aea3ee7700..da51cdca9c 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -137,7 +137,8 @@ extension RUMViewEvent: RandomMockable { viewIsActive: Bool? = .random(), viewTimeSpent: Int64 = .mockRandom(), viewURL: String = .mockRandom(), - crashCount: Int64? = nil + crashCount: Int64? = nil, + hasReplay: Bool? = nil ) -> RUMViewEvent { return RUMViewEvent( dd: .init( @@ -165,7 +166,7 @@ extension RUMViewEvent: RandomMockable { privacy: nil, service: .mockRandom(), session: .init( - hasReplay: nil, + hasReplay: hasReplay, id: sessionID.toRUMDataFormat, isActive: true, sampledForReplay: nil, diff --git a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift index 9145d1b56b..d945c4a23e 100644 --- a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift @@ -147,6 +147,7 @@ struct RUMCommandMock: RUMCommand { var attributes: [AttributeKey: AttributeValue] = [:] var canStartBackgroundView = false var isUserInteraction = false + var missedEventType: SessionEndedMetric.MissedEventType? = nil } /// Creates random `RUMCommand` from available ones. @@ -218,14 +219,16 @@ extension RUMStartViewCommand: AnyMockable, RandomMockable { attributes: [AttributeKey: AttributeValue] = [:], identity: ViewIdentifier = .mockViewIdentifier(), name: String = .mockAny(), - path: String = .mockAny() + path: String = .mockAny(), + instrumentationType: SessionEndedMetric.ViewInstrumentationType = .manual ) -> RUMStartViewCommand { return RUMStartViewCommand( time: time, identity: identity, name: name, path: path, - attributes: attributes + attributes: attributes, + instrumentationType: instrumentationType ) } } @@ -772,7 +775,8 @@ extension RUMScopeDependencies { onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), viewCache: ViewCache = ViewCache(), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), - sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()) + sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()), + watchdogTermination: WatchdogTerminationMonitor = .mockRandom() ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: featureScope, @@ -790,7 +794,8 @@ extension RUMScopeDependencies { onSessionStart: onSessionStart, viewCache: viewCache, fatalErrorContext: fatalErrorContext, - sessionEndedMetric: sessionEndedMetric + sessionEndedMetric: sessionEndedMetric, + watchdogTermination: watchdogTermination ) } @@ -810,7 +815,8 @@ extension RUMScopeDependencies { onSessionStart: RUM.SessionListener? = nil, viewCache: ViewCache? = nil, fatalErrorContext: FatalErrorContextNotifying? = nil, - sessionEndedMetric: SessionEndedMetricController? = nil + sessionEndedMetric: SessionEndedMetricController? = nil, + watchdogTermination: WatchdogTerminationMonitor? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: self.featureScope, @@ -828,7 +834,8 @@ extension RUMScopeDependencies { onSessionStart: onSessionStart ?? self.onSessionStart, viewCache: viewCache ?? self.viewCache, fatalErrorContext: fatalErrorContext ?? self.fatalErrorContext, - sessionEndedMetric: sessionEndedMetric ?? self.sessionEndedMetric + sessionEndedMetric: sessionEndedMetric ?? self.sessionEndedMetric, + watchdogTermination: watchdogTermination ) } } diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 4c63c5d0d1..2eb6013b39 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -623,9 +623,9 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual( randomCommandLog, """ - \(String(describing: randomCommand)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. + \(String(describing: randomCommand)) was detected, but no view is active. To track views automatically, configure + `RUM.Configuration.uiKitViewsPredicate` or use `.trackRUMView()` modifier in SwiftUI. You can also track views manually + with `RUMMonitor.shared().startView()` and `RUMMonitor.shared().stopView()`. """ ) diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift index 9452988c7f..6f3829761a 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricControllerTests.swift @@ -19,18 +19,20 @@ class SessionEndedMetricControllerTests: XCTestCase { // Given let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // When - viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), in: sessionID) } + viewIDs.forEach { controller.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil, in: sessionID) } errorKinds.forEach { controller.track(sdkErrorKind: $0, in: sessionID) } + controller.track(missedEventType: .action, in: sessionID) controller.trackWasStopped(sessionID: sessionID) - controller.endMetric(sessionID: sessionID) + controller.endMetric(sessionID: sessionID, with: .mockRandom()) // Then let metric = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) XCTAssertEqual(metric.viewsCount.total, viewIDs.count) XCTAssertEqual(metric.sdkErrorsCount.total, errorKinds.count) + XCTAssertEqual(metric.noViewEventsCount.actions, 1) XCTAssertEqual(metric.wasStopped, true) } @@ -40,18 +42,18 @@ class SessionEndedMetricControllerTests: XCTestCase { // When let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) - controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) + controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // Session 1: - controller.track(view: .mockRandomWith(sessionID: sessionID1), in: sessionID1) + controller.track(view: .mockRandomWith(sessionID: sessionID1), instrumentationType: nil, in: sessionID1) controller.track(sdkErrorKind: "error.kind1", in: sessionID1) controller.trackWasStopped(sessionID: sessionID1) // Session 2: controller.track(sdkErrorKind: "error.kind2", in: sessionID2) // Send 1st and 2nd: - controller.endMetric(sessionID: sessionID1) + controller.endMetric(sessionID: sessionID1, with: .mockRandom()) let metric1 = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) - controller.endMetric(sessionID: sessionID2) + controller.endMetric(sessionID: sessionID2, with: .mockRandom()) let metric2 = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) // Then @@ -72,19 +74,21 @@ class SessionEndedMetricControllerTests: XCTestCase { // When let controller = SessionEndedMetricController(telemetry: telemetry) - controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom()) - controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom()) + controller.startMetric(sessionID: sessionID1, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) + controller.startMetric(sessionID: sessionID2, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom()) // Track latest session (`sessionID: nil`) - controller.track(view: .mockRandomWith(sessionID: sessionID2), in: nil) + controller.track(view: .mockRandomWith(sessionID: sessionID2), instrumentationType: nil, in: nil) controller.track(sdkErrorKind: "error.kind1", in: nil) + controller.track(missedEventType: .resource, in: nil) controller.trackWasStopped(sessionID: nil) // Send 2nd: - controller.endMetric(sessionID: sessionID2) + controller.endMetric(sessionID: sessionID2, with: .mockRandom()) let metric = try XCTUnwrap(telemetry.messages.lastSessionEndedMetric) // Then XCTAssertEqual(metric.viewsCount.total, 1) XCTAssertEqual(metric.sdkErrorsCount.total, 1) + XCTAssertEqual(metric.noViewEventsCount.resources, 1) XCTAssertEqual(metric.wasStopped, true) } @@ -98,15 +102,17 @@ class SessionEndedMetricControllerTests: XCTestCase { callConcurrently( closures: [ { controller.startMetric( - sessionID: sessionIDs.randomElement()!, precondition: .mockRandom(), context: .mockRandom() + sessionID: sessionIDs.randomElement()!, precondition: .mockRandom(), context: .mockRandom(), tracksBackgroundEvents: .mockRandom() ) }, - { controller.track(view: .mockRandom(), in: sessionIDs.randomElement()!) }, + { controller.track(view: .mockRandom(), instrumentationType: nil, in: sessionIDs.randomElement()!) }, { controller.track(sdkErrorKind: .mockRandom(), in: sessionIDs.randomElement()!) }, { controller.trackWasStopped(sessionID: sessionIDs.randomElement()!) }, - { controller.track(view: .mockRandom(), in: nil) }, + { controller.track(view: .mockRandom(), instrumentationType: nil, in: nil) }, { controller.track(sdkErrorKind: .mockRandom(), in: nil) }, + { controller.track(missedEventType: .action, in: sessionIDs.randomElement()!) }, + { controller.track(missedEventType: .resource, in: nil) }, { controller.trackWasStopped(sessionID: nil) }, - { controller.endMetric(sessionID: sessionIDs.randomElement()!) }, + { controller.endMetric(sessionID: sessionIDs.randomElement()!, with: .mockRandom()) }, ], iterations: 100 ) diff --git a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift index a0d0e5846a..d109d40259 100644 --- a/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift +++ b/DatadogRUM/Tests/SDKMetrics/SessionEndedMetricTests.swift @@ -16,7 +16,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingEmptyMetric() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -35,7 +35,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingMetricType() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -48,7 +48,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionID() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -61,9 +61,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingAppProcessType() throws { // Given - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: .mockRandom(), context: .mockWith(applicationBundleType: .iOSApp) - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSApp)) // When let attributes = metric.asMetricAttributes() @@ -75,9 +73,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingExtensionProcessType() throws { // Given - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: .mockRandom(), context: .mockWith(applicationBundleType: .iOSAppExtension) - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(applicationBundleType: .iOSAppExtension)) // When let attributes = metric.asMetricAttributes() @@ -92,9 +88,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionPrecondition() throws { // Given let expectedPrecondition: RUMSessionPrecondition = .mockRandom() - let metric = SessionEndedMetric( - sessionID: sessionID, precondition: expectedPrecondition, context: .mockRandom() - ) + let metric = SessionEndedMetric.with(sessionID: sessionID, precondition: expectedPrecondition) // When let attributes = metric.asMetricAttributes() @@ -104,15 +98,30 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.precondition, expectedPrecondition.rawValue) } + // MARK: - Tracks Background Events + + func testReportingTracksBackgroundEvents() throws { + // Given + let expected: Bool = .mockRandom() + let metric = SessionEndedMetric.with(sessionID: sessionID, tracksBackgroundEvents: expected) + + // When + let attributes = metric.asMetricAttributes() + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.hasBackgroundEventsTrackingEnabled, expected) + } + // MARK: - Duration func testComputingDurationFromSingleView() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view: RUMViewEvent = .mockRandomWith(sessionID: sessionID) // When - try metric.track(view: view) + try metric.track(view: view, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -122,15 +131,15 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromMultipleViews() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) let view3: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms + 10.s2ms + 20.s2ms, viewTimeSpent: 50.s2ns) // When - try metric.track(view: view1) - try metric.track(view: view2) - try metric.track(view: view3) + try metric.track(view: view1, instrumentationType: nil) + try metric.track(view: view2, instrumentationType: nil) + try metric.track(view: view3, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -140,13 +149,13 @@ class SessionEndedMetricTests: XCTestCase { func testComputingDurationFromOverlappingViews() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) let view1: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 10.s2ms, viewTimeSpent: 10.s2ns) let view2: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 15.s2ms, viewTimeSpent: 20.s2ns) // starts in the middle of `view1` // When - try metric.track(view: view1) - try metric.track(view: view2) + try metric.track(view: view1, instrumentationType: nil) + try metric.track(view: view2, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -156,14 +165,14 @@ class SessionEndedMetricTests: XCTestCase { func testDurationIsAlwaysComputedFromTheFirstAndLastView() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) let firstView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms, viewTimeSpent: 10.s2ns) let lastView: RUMViewEvent = .mockRandomWith(sessionID: sessionID, date: 5.s2ms + 10.s2ms, viewTimeSpent: 20.s2ns) // When - try metric.track(view: firstView) - try (0..<10).forEach { _ in try metric.track(view: .mockRandomWith(sessionID: sessionID)) } // middle views should not alter the duration - try metric.track(view: lastView) + try metric.track(view: firstView, instrumentationType: nil) + try (0..<10).forEach { _ in try metric.track(view: .mockRandomWith(sessionID: sessionID), instrumentationType: nil) } // middle views should not alter the duration + try metric.track(view: lastView, instrumentationType: nil) let attributes = metric.asMetricAttributes() // Then @@ -173,11 +182,11 @@ class SessionEndedMetricTests: XCTestCase { func testWhenComputingDuration_itIgnoresViewsFromDifferentSession() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When - XCTAssertThrowsError(try metric.track(view: .mockRandom())) - XCTAssertThrowsError(try metric.track(view: .mockRandom())) + XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) + XCTAssertThrowsError(try metric.track(view: .mockRandom(), instrumentationType: nil)) let attributes = metric.asMetricAttributes() // Then @@ -189,7 +198,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionThatWasStopped() throws { // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When metric.trackWasStopped() @@ -202,7 +211,7 @@ class SessionEndedMetricTests: XCTestCase { func testReportingSessionThatWasNotStopped() throws { // Given - let metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When let attributes = metric.asMetricAttributes() @@ -212,16 +221,34 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertFalse(rse.wasStopped) } + // MARK: - NTP Offset + + func testReportingNTPOffset() throws { + let offsetAtStart: TimeInterval = .mockRandom(min: -10, max: 10) + let offsetAtEnd: TimeInterval = .mockRandom(min: -10, max: 10) + + // Given + let metric = SessionEndedMetric.with(sessionID: sessionID, context: .mockWith(serverTimeOffset: offsetAtStart)) + + // When + let attributes = metric.asMetricAttributes(with: .mockWith(serverTimeOffset: offsetAtEnd)) + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.ntpOffset.atStart, offsetAtStart.toInt64Milliseconds) + XCTAssertEqual(rse.ntpOffset.atEnd, offsetAtEnd.toInt64Milliseconds) + } + // MARK: - Views Count func testReportingTotalViewsCount() throws { let viewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When - try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0)) } + try viewIDs.forEach { try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: $0), instrumentationType: nil) } let attributes = metric.asMetricAttributes() // Then @@ -229,18 +256,37 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.viewsCount.total, viewIDs.count) } - func testReportingBackgorundViewsCount() throws { + func testWhenReportingTotalViewsCount_itCountsEachViewIDOnlyOnce() throws { + let viewID1: String = .mockRandom() + let viewID2: String = .mockRandom(otherThan: [viewID1]) + + // Given + let metric = SessionEndedMetric.with(sessionID: sessionID) + + // When + try (0..<5).forEach { _ in // repeat few times + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID1), instrumentationType: nil) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID2), instrumentationType: nil) + } + let attributes = metric.asMetricAttributes() + + // Then + let rse = try XCTUnwrap(attributes[Constants.rseKey] as? SessionEndedAttributes) + XCTAssertEqual(rse.viewsCount.total, 2) + } + + func testReportingBackgroundViewsCount() throws { let backgroundViewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) let otherViewIDs: Set = .mockRandom(count: .mockRandom(min: 1, max: 10)) let viewIDs = backgroundViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in let viewURL = backgroundViewIDs.contains(viewID) ? RUMOffViewEventsHandlingRule.Constants.backgroundViewURL : .mockRandom() - try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL)) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL), instrumentationType: nil) } let attributes = metric.asMetricAttributes() @@ -255,12 +301,12 @@ class SessionEndedMetricTests: XCTestCase { let viewIDs = appLaunchViewIDs.union(otherViewIDs) // Given - var metric = SessionEndedMetric(sessionID: sessionID, precondition: .mockRandom(), context: .mockRandom()) + let metric = SessionEndedMetric.with(sessionID: sessionID) // When try viewIDs.forEach { viewID in let viewURL = appLaunchViewIDs.contains(viewID) ? RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL : .mockRandom() - try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL)) + try metric.track(view: .mockRandomWith(sessionID: sessionID, viewID: viewID, viewURL: viewURL), instrumentationType: nil) } let attributes = metric.asMetricAttributes() @@ -269,13 +315,85 @@ class SessionEndedMetricTests: XCTestCase { XCTAssertEqual(rse.viewsCount.applicationLaunch, appLaunchViewIDs.count) } - func testReportingViewsCount_itIgnoresViewsFromDifferentSession() throws { + func testReportingViewsCountByInstrumentationType() throws { + let manualViewsCount: Int = .mockRandom(min: 1, max: 10) + let swiftuiViewsCount: Int = .mockRandom(min: 1, max: 10) + let uikitViewsCount: Int = .mockRandom(min: 1, max: 10) + let unknownViewsCount: Int = .mockRandom(min: 1, max: 10) + + // Given + let metric = SessionEndedMetric.with(sessionID: sessionID) + + // When + try (0.. SessionEndedMetric { + SessionEndedMetric(sessionID: sessionID, precondition: precondition, context: context, tracksBackgroundEvents: tracksBackgroundEvents) + } + + func asMetricAttributes() -> [String: Encodable] { + asMetricAttributes(with: .mockRandom()) + } +} diff --git a/DatadogSDK.podspec b/DatadogSDK.podspec index 1d26951b26..9878a9069f 100644 --- a/DatadogSDK.podspec +++ b/DatadogSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogSDK" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKAlamofireExtension.podspec b/DatadogSDKAlamofireExtension.podspec index e79013aac6..31e1af20d5 100644 --- a/DatadogSDKAlamofireExtension.podspec +++ b/DatadogSDKAlamofireExtension.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKAlamofireExtension" s.module_name = "DatadogAlamofireExtension" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKCrashReporting.podspec b/DatadogSDKCrashReporting.podspec index fd38b0dbc1..d60c448d0c 100644 --- a/DatadogSDKCrashReporting.podspec +++ b/DatadogSDKCrashReporting.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKCrashReporting" s.module_name = "DatadogCrashReporting" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKObjc.podspec b/DatadogSDKObjc.podspec index 5a4d1b83bd..fa8eef30f9 100644 --- a/DatadogSDKObjc.podspec +++ b/DatadogSDKObjc.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKObjc" s.module_name = "DatadogObjc" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSessionReplay.podspec b/DatadogSessionReplay.podspec index cbb9450b3c..f9c1b4d131 100644 --- a/DatadogSessionReplay.podspec +++ b/DatadogSessionReplay.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogSessionReplay" - s.version = "2.13.0" + s.version = "2.14.0" s.summary = "Official Datadog Session Replay SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Fixtures.swift b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Fixtures.swift index a2b7ead57d..1438276ea0 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Fixtures.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Fixtures.swift @@ -11,6 +11,8 @@ public enum Fixture: CaseIterable { case basicShapes case basicTexts case sliders + case progressViews + case activityIndicators case segments case pickers case switches @@ -54,6 +56,10 @@ public enum Fixture: CaseIterable { return UIStoryboard.basic.instantiateViewController(withIdentifier: "Texts") case .sliders: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Sliders") + case .progressViews: + return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "ProgressViews") + case .activityIndicators: + return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "ActivityIndicators") case .segments: return UIStoryboard.inputElements.instantiateViewController(withIdentifier: "Segments") case .pickers: diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard index 11f5e78166..17c11237d6 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Images.storyboard @@ -1,9 +1,9 @@ - + - + @@ -156,33 +156,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -