diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5bed1f3ac..2f187a2865 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,10 +10,4 @@ A brief description of implementation details of this PR. - [ ] Feature or bugfix MUST have appropriate tests (unit, integration) - [ ] Make sure each commit and the PR mention the Issue number or JIRA reference - [ ] Add CHANGELOG entry for user facing changes - -### 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/` +- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) [internal]) and run `make api-surface`) diff --git a/.gitignore b/.gitignore index 8f354915c8..896abc144a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,9 @@ Carthage/Build Carthage/Checkouts xcuserdata/ -instrumented-tests/DatadogSDKTesting.* -instrumented-tests/LICENSE - *.local.xcconfig +E2ETests/code-signing +tools/dogfooding/repos # Ignore files for Python tools: .idea @@ -20,3 +19,7 @@ __pycache__ *.swp .venv .vscode +*.pytest_cache + +# CI job artifacts +artifacts/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddf3917437..17427b4de0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,86 +1,391 @@ stages: - - info + - pre - lint - test + - ui-test + - smoke-test + - e2e-test + - benchmark-test + - dogfood + - release-build + - release-publish + - post -ENV info: - stage: info +variables: + MAIN_BRANCH: "master" + DEVELOP_BRANCH: "develop" + # Default Xcode and runtime versions for all jobs: + DEFAULT_XCODE: "15.4.0" + DEFAULT_IOS_OS: "17.5" + DEFAULT_TVOS_OS: "17.5" + # Prefilled variables for running a pipeline manually: + # Ref.: https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines + RELEASE_GIT_TAG: + description: "The Git tag for the release pipeline. If set, release pipeline will be triggered for the given tag." + RELEASE_DRY_RUN: + value: "1" + description: "Controls the dry run mode for the release pipeline. If set to '1', the pipeline will execute all steps but will not publish artifacts. If set to '0', the pipeline will run fully." + +default: tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs - 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 + - macos:sonoma + - specific:true + +# ┌───────────────┐ +# │ Utility jobs: │ +# └───────────────┘ + +# Utility jobs define rules for including or excluding dependent jobs from the pipeline. +# +# Ref.: https://docs.gitlab.com/ee/ci/jobs/job_rules.html +# > Rules are evaluated in order until the first match. When a match is found, the job is either included or excluded +# > from the pipeline, depending on the configuration. + +.test-pipeline-job: + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH || $CI_COMMIT_BRANCH == $MAIN_BRANCH' # always on main branches + - if: '$CI_COMMIT_BRANCH' # when on other branch with following changes compared to develop + changes: + paths: + - "Datadog*/**/*" + - "IntegrationTests/**/*" + - "SmokeTests/**/*" + - "TestUtilities/**/*" + - "*" # match any file in the root directory + compare_to: 'develop' # cannot use $DEVELOP_BRANCH var due to: https://gitlab.com/gitlab-org/gitlab/-/issues/369916 + +.release-pipeline-job: + rules: + - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' + +.release-pipeline-20m-delayed-job: + rules: + - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' + when: delayed + start_in: 20 minutes + +.release-pipeline-40m-delayed-job: + rules: + - if: '$CI_COMMIT_TAG || $RELEASE_GIT_TAG' + when: delayed + start_in: 40 minutes + +ENV check: + stage: pre + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + script: + - ./tools/runner-setup.sh --datadog-ci + - make env-check + +# ┌──────────────────────────┐ +# │ SDK changes integration: │ +# └──────────────────────────┘ Lint: stage: lint - tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + rules: + - !reference [.test-pipeline-job, 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 [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.0.1" - 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): + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make clean repo-setup ENV=ci + - make test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 + +Unit Tests (tvOS): stage: test - tags: - - mac-ventura-preview - allow_failure: true # do not block GH PRs + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] variables: - TEST_WORKSPACE: "Datadog.xcworkspace" - TEST_DESTINATION: "platform=tvOS Simulator,name=Apple TV,OS=17.0" - 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): + PLATFORM: "tvOS Simulator" + DEVICE: "Apple TV" + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make clean repo-setup ENV=ci + - make test-tvos-all OS="$DEFAULT_TVOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 + +UI Tests: + stage: ui-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + variables: + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + parallel: + matrix: + - TEST_PLAN: + - Default + - RUM + - CrashReporting + - NetworkInstrumentation + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make clean repo-setup ENV=ci + - make ui-test TEST_PLAN="$TEST_PLAN" OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +SR Snapshot Tests: + stage: ui-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + variables: + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15" + ARTIFACTS_PATH: "artifacts" + artifacts: + paths: + - artifacts + expire_in: 1 week + when: on_failure + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --ssh + - make clean repo-setup ENV=ci + - make sr-snapshots-pull sr-snapshot-test OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" ARTIFACTS_PATH="$ARTIFACTS_PATH" + +Tools Tests: stage: test + rules: + - if: '$CI_COMMIT_BRANCH' # when on branch with following changes compared to develop + changes: + paths: + - "tools/**/*" + - "Makefile" + - ".gitlab-ci.yml" + compare_to: 'develop' + script: + - make clean repo-setup ENV=ci + - make tools-test + +Benchmark Build: + stage: smoke-test + rules: + - if: '$CI_COMMIT_BRANCH' # when on branch with following changes compared to develop + changes: + paths: + - "BenchmarkTests/**/*" + compare_to: 'develop' + script: + - make benchmark-build + +Smoke Tests (iOS): + stage: smoke-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + variables: + PLATFORM: "iOS Simulator" + DEVICE: "iPhone 15 Pro" + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --ssh + - make clean repo-setup ENV=ci + - make smoke-test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +Smoke Tests (tvOS): + stage: smoke-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + variables: + PLATFORM: "tvOS Simulator" + DEVICE: "Apple TV" + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --ssh + - make clean repo-setup ENV=ci + - make smoke-test-tvos-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" + +SPM Build (Swift 5.10): + stage: smoke-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --iOS --tvOS --visionOS --watchOS + - make clean repo-setup ENV=ci + - make spm-build-ios + - make spm-build-tvos + - make spm-build-visionos + - make spm-build-macos + - make spm-build-watchos + +SPM Build (Swift 5.9): + stage: smoke-test + rules: + - !reference [.test-pipeline-job, rules] + - !reference [.release-pipeline-job, rules] 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" - 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 + XCODE: "15.2.0" + script: + - ./tools/runner-setup.sh --xcode "$XCODE" --iOS --tvOS --visionOS --watchOS + - make clean repo-setup ENV=ci + - make spm-build-ios + - make spm-build-tvos + - make spm-build-visionos + - make spm-build-macos + - make spm-build-watchos + +# ┌──────────────────────┐ +# │ E2E Test app upload: │ +# └──────────────────────┘ + +E2E Test (upload to s8s): + stage: e2e-test + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + artifacts: + paths: + - artifacts + expire_in: 2 weeks + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --datadog-ci + - make clean + - export DRY_RUN=${DRY_RUN:-0} # default to 0 if not specified + - make e2e-upload ARTIFACTS_PATH="artifacts/e2e" + +# ┌────────────────────────────┐ +# │ Benchmark Test app upload: │ +# └────────────────────────────┘ + +Benchmark Test (upload to s8s): + stage: benchmark-test + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + allow_failure: true + artifacts: + paths: + - artifacts + expire_in: 2 weeks + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --datadog-ci + - make clean + - export DRY_RUN=${DRY_RUN:-0} # default to 0 if not specified + - make benchmark-upload ARTIFACTS_PATH="artifacts/benchmark" + +# ┌─────────────────┐ +# │ SDK dogfooding: │ +# └─────────────────┘ + +Dogfood (Shopist): + stage: dogfood + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + when: manual + allow_failure: true + script: + - ./tools/runner-setup.sh --ssh + - DRY_RUN=0 make dogfood-shopist + +Dogfood (Datadog app): + stage: dogfood + rules: + - if: '$CI_COMMIT_BRANCH == $DEVELOP_BRANCH' + when: manual + allow_failure: true + script: + - ./tools/runner-setup.sh --ssh + - DRY_RUN=0 make dogfood-datadog-app + +# ┌──────────────┐ +# │ SDK release: │ +# └──────────────┘ + +.release-before-script: &export_MAKE_release_params + - export GIT_TAG=${RELEASE_GIT_TAG:-$CI_COMMIT_TAG} # CI_COMMIT_TAG if set, otherwise default to RELEASE_GIT_TAG + - if [ -z "$GIT_TAG" ]; then echo "GIT_TAG is not set"; exit 1; fi # sanity check + - export ARTIFACTS_PATH="artifacts/$GIT_TAG" + - export DRY_RUN=${CI_COMMIT_TAG:+0} # 0 if CI_COMMIT_TAG is set + - export DRY_RUN=${DRY_RUN:-$RELEASE_DRY_RUN} # otherwise default to RELEASE_DRY_RUN + +Build Artifacts: + stage: release-build + rules: + - !reference [.release-pipeline-job, rules] + artifacts: + paths: + - artifacts + expire_in: 4 weeks + before_script: + - *export_MAKE_release_params + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" --ssh + - make env-check + - make clean + - make release-build release-validate + +Publish GH Asset: + stage: release-publish + rules: + - !reference [.release-pipeline-job, rules] + before_script: + - *export_MAKE_release_params + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make env-check + - make clean + - make release-publish-github + +Publish CP podspecs (internal): + stage: release-publish + rules: + - !reference [.release-pipeline-job, rules] + before_script: + - *export_MAKE_release_params + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make env-check + - make clean + - make release-publish-internal-podspecs + +Publish CP podspecs (dependent): + stage: release-publish + rules: + - !reference [.release-pipeline-20m-delayed-job, rules] + before_script: + - *export_MAKE_release_params + needs: ["Build Artifacts", "Publish CP podspecs (internal)"] + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make env-check + - make clean + - make release-publish-dependent-podspecs + +Publish CP podspecs (legacy): + stage: release-publish + rules: + - !reference [.release-pipeline-40m-delayed-job, rules] + before_script: + - *export_MAKE_release_params + needs: ["Build Artifacts", "Publish CP podspecs (dependent)"] + script: + - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" + - make env-check + - make clean + - make release-publish-legacy-podspecs + +# ┌────────────────┐ +# │ Notifications: │ +# └────────────────┘ + +# This job runs at the end of every successful pipeline. +# It syncs the GitLab pipeline status with GitHub status checks. +Sync GH Checks: + stage: post + script: + - echo "All good" diff --git a/BenchmarkTests/BenchmarkTests.xcodeproj/.xcodesamplecode.plist b/BenchmarkTests/BenchmarkTests.xcodeproj/.xcodesamplecode.plist new file mode 100644 index 0000000000..4bc741ca64 --- /dev/null +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/.xcodesamplecode.plist @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj b/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..70b3105e82 --- /dev/null +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/project.pbxproj @@ -0,0 +1,1277 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + D231DC372C73355800F3F66C /* UIKitCatalog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; }; + D231DC382C73355800F3F66C /* UIKitCatalog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D231DCAF2C73356E00F3F66C /* ActivityIndicatorViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */; }; + D231DCB02C73356E00F3F66C /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */; }; + D231DCB12C73356E00F3F66C /* AlertControllerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */; }; + D231DCB22C73356E00F3F66C /* AlertControllerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */; }; + D231DCB42C73356E00F3F66C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D231DC472C73356D00F3F66C /* Assets.xcassets */; }; + D231DCB52C73356E00F3F66C /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC482C73356D00F3F66C /* BaseTableViewController.swift */; }; + D231DCB62C73356E00F3F66C /* ButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */; }; + D231DCB72C73356E00F3F66C /* ButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */; }; + D231DCB82C73356E00F3F66C /* ButtonViewController+Configs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */; }; + D231DCB92C73356E00F3F66C /* CaseElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC4D2C73356D00F3F66C /* CaseElement.swift */; }; + D231DCBA2C73356E00F3F66C /* ColorPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */; }; + D231DCBB2C73356E00F3F66C /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */; }; + D231DCBC2C73356E00F3F66C /* content.html in Resources */ = {isa = PBXBuildFile; fileRef = D231DC522C73356D00F3F66C /* content.html */; }; + D231DCBD2C73356E00F3F66C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = D231DC542C73356D00F3F66C /* Credits.rtf */; }; + D231DCBE2C73356E00F3F66C /* CustomPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */; }; + D231DCBF2C73356E00F3F66C /* CustomPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */; }; + D231DCC02C73356E00F3F66C /* CustomSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */; }; + D231DCC12C73356E00F3F66C /* CustomSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */; }; + D231DCC22C73356E00F3F66C /* CustomToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */; }; + D231DCC32C73356E00F3F66C /* CustomToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */; }; + D231DCC42C73356E00F3F66C /* DatePickerController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */; }; + D231DCC52C73356E00F3F66C /* DatePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC602C73356D00F3F66C /* DatePickerController.swift */; }; + D231DCC62C73356E00F3F66C /* DefaultPageControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */; }; + D231DCC72C73356E00F3F66C /* DefaultPageControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */; }; + D231DCC82C73356E00F3F66C /* DefaultSearchBarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */; }; + D231DCC92C73356E00F3F66C /* DefaultSearchBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */; }; + D231DCCA2C73356E00F3F66C /* DefaultToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */; }; + D231DCCB2C73356E00F3F66C /* DefaultToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */; }; + D231DCCC2C73356E00F3F66C /* FontPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */; }; + D231DCCD2C73356E00F3F66C /* FontPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */; }; + D231DCCE2C73356E00F3F66C /* ImagePickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */; }; + D231DCCF2C73356E00F3F66C /* ImagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */; }; + D231DCD02C73356E00F3F66C /* ImageViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC712C73356D00F3F66C /* ImageViewController.storyboard */; }; + D231DCD12C73356E00F3F66C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC722C73356D00F3F66C /* ImageViewController.swift */; }; + D231DCD42C73356E00F3F66C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D231DC782C73356D00F3F66C /* Localizable.strings */; }; + D231DCD52C73356E00F3F66C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC7A2C73356D00F3F66C /* Main.storyboard */; }; + D231DCD62C73356E00F3F66C /* MenuButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */; }; + D231DCD72C73356E00F3F66C /* MenuButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */; }; + D231DCD82C73356E00F3F66C /* OutlineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */; }; + D231DCD92C73356E00F3F66C /* PickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC802C73356D00F3F66C /* PickerViewController.storyboard */; }; + D231DCDA2C73356E00F3F66C /* PickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC812C73356D00F3F66C /* PickerViewController.swift */; }; + D231DCDB2C73356E00F3F66C /* PointerInteractionButtonViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */; }; + D231DCDC2C73356E00F3F66C /* PointerInteractionButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */; }; + D231DCDD2C73356E00F3F66C /* ProgressViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */; }; + D231DCDE2C73356E00F3F66C /* ProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC872C73356D00F3F66C /* ProgressViewController.swift */; }; + D231DCE02C73356E00F3F66C /* SegmentedControlViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */; }; + D231DCE12C73356E00F3F66C /* SegmentedControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */; }; + D231DCE22C73356E00F3F66C /* SliderViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */; }; + D231DCE32C73356E00F3F66C /* SliderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC8E2C73356D00F3F66C /* SliderViewController.swift */; }; + D231DCE42C73356E00F3F66C /* StackViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC902C73356D00F3F66C /* StackViewController.storyboard */; }; + D231DCE52C73356E00F3F66C /* StackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC912C73356D00F3F66C /* StackViewController.swift */; }; + D231DCE62C73356E00F3F66C /* StepperViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC932C73356D00F3F66C /* StepperViewController.storyboard */; }; + D231DCE72C73356E00F3F66C /* StepperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC942C73356D00F3F66C /* StepperViewController.swift */; }; + D231DCE82C73356E00F3F66C /* SwitchViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */; }; + D231DCE92C73356E00F3F66C /* SwitchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC972C73356D00F3F66C /* SwitchViewController.swift */; }; + D231DCEA2C73356E00F3F66C /* SymbolViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */; }; + D231DCEB2C73356E00F3F66C /* SymbolViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */; }; + D231DCEC2C73356E00F3F66C /* TextFieldViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */; }; + D231DCED2C73356E00F3F66C /* TextFieldViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */; }; + D231DCEE2C73356E00F3F66C /* TextViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */; }; + D231DCEF2C73356E00F3F66C /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA02C73356D00F3F66C /* TextViewController.swift */; }; + D231DCF02C73356E00F3F66C /* TintedToolbarViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */; }; + D231DCF12C73356E00F3F66C /* TintedToolbarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */; }; + D231DCF32C73356E00F3F66C /* VisualEffectViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */; }; + D231DCF42C73356E00F3F66C /* VisualEffectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */; }; + D231DCF52C73356E00F3F66C /* WebViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */; }; + D231DCF62C73356E00F3F66C /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCAB2C73356D00F3F66C /* WebViewController.swift */; }; + D231DCF92C7342D500F3F66C /* ModuleBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D231DCF82C7342D500F3F66C /* ModuleBundle.swift */; }; + D23DD32D2C58D80C00B90C4C /* DatadogBenchmarks in Frameworks */ = {isa = PBXBuildFile; productRef = D23DD32C2C58D80C00B90C4C /* DatadogBenchmarks */; }; + D24BFD472C6B916B00AB9604 /* SyntheticScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */; }; + D24E15F32C776956005AE4E8 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */; }; + D27606A12C514F37002D2A14 /* SessionReplayScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27606982C514F37002D2A14 /* SessionReplayScenario.swift */; }; + D27606A32C514F37002D2A14 /* Scenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = D276069B2C514F37002D2A14 /* Scenario.swift */; }; + D27606A42C514F37002D2A14 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D276069D2C514F37002D2A14 /* AppConfiguration.swift */; }; + D27606A72C514F77002D2A14 /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = D27606A62C514F77002D2A14 /* DatadogCore */; }; + D27606A92C514F77002D2A14 /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = D27606A82C514F77002D2A14 /* DatadogLogs */; }; + D27606AB2C514F77002D2A14 /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = D27606AA2C514F77002D2A14 /* DatadogRUM */; }; + D27606AD2C514F77002D2A14 /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = D27606AC2C514F77002D2A14 /* DatadogSessionReplay */; }; + D27606AF2C514F77002D2A14 /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = D27606AE2C514F77002D2A14 /* DatadogTrace */; }; + D29F75502C4AA07E00288638 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29F754F2C4AA07E00288638 /* AppDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + D231DC352C73355800F3F66C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D29F75282C4A9EFA00288638 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D231DC302C73355800F3F66C; + remoteInfo = UIKitCatalog; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + D29F75872C4AA98F00288638 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D231DC382C73355800F3F66C /* UIKitCatalog.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + D231DC312C73355800F3F66C /* UIKitCatalog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UIKitCatalog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D231DC402C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ActivityIndicatorViewController.storyboard; sourceTree = ""; }; + D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; + D231DC432C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AlertControllerViewController.storyboard; sourceTree = ""; }; + D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertControllerViewController.swift; sourceTree = ""; }; + D231DC472C73356D00F3F66C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D231DC482C73356D00F3F66C /* BaseTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; + D231DC492C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ButtonViewController.storyboard; sourceTree = ""; }; + D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonViewController.swift; sourceTree = ""; }; + D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ButtonViewController+Configs.swift"; sourceTree = ""; }; + D231DC4D2C73356D00F3F66C /* CaseElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseElement.swift; sourceTree = ""; }; + D231DC4E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ColorPickerViewController.storyboard; sourceTree = ""; }; + D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; + D231DC512C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = Base.lproj/content.html; sourceTree = ""; }; + D231DC532C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = ""; }; + D231DC552C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomPageControlViewController.storyboard; sourceTree = ""; }; + D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPageControlViewController.swift; sourceTree = ""; }; + D231DC582C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomSearchBarViewController.storyboard; sourceTree = ""; }; + D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSearchBarViewController.swift; sourceTree = ""; }; + D231DC5B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomToolbarViewController.storyboard; sourceTree = ""; }; + D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomToolbarViewController.swift; sourceTree = ""; }; + D231DC5E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DatePickerController.storyboard; sourceTree = ""; }; + D231DC602C73356D00F3F66C /* DatePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerController.swift; sourceTree = ""; }; + D231DC612C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultPageControlViewController.storyboard; sourceTree = ""; }; + D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultPageControlViewController.swift; sourceTree = ""; }; + D231DC642C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultSearchBarViewController.storyboard; sourceTree = ""; }; + D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSearchBarViewController.swift; sourceTree = ""; }; + D231DC672C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DefaultToolbarViewController.storyboard; sourceTree = ""; }; + D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultToolbarViewController.swift; sourceTree = ""; }; + D231DC6A2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/FontPickerViewController.storyboard; sourceTree = ""; }; + D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontPickerViewController.swift; sourceTree = ""; }; + D231DC6D2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImagePickerViewController.storyboard; sourceTree = ""; }; + D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerViewController.swift; sourceTree = ""; }; + D231DC702C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ImageViewController.storyboard; sourceTree = ""; }; + D231DC722C73356D00F3F66C /* ImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; + D231DC772C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; + D231DC792C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D231DC7B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MenuButtonViewController.storyboard; sourceTree = ""; }; + D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuButtonViewController.swift; sourceTree = ""; }; + D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineViewController.swift; sourceTree = ""; }; + D231DC7F2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PickerViewController.storyboard; sourceTree = ""; }; + D231DC812C73356D00F3F66C /* PickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerViewController.swift; sourceTree = ""; }; + D231DC822C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PointerInteractionButtonViewController.storyboard; sourceTree = ""; }; + D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointerInteractionButtonViewController.swift; sourceTree = ""; }; + D231DC852C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ProgressViewController.storyboard; sourceTree = ""; }; + D231DC872C73356D00F3F66C /* ProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewController.swift; sourceTree = ""; }; + D231DC892C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SegmentedControlViewController.storyboard; sourceTree = ""; }; + D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlViewController.swift; sourceTree = ""; }; + D231DC8C2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SliderViewController.storyboard; sourceTree = ""; }; + D231DC8E2C73356D00F3F66C /* SliderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderViewController.swift; sourceTree = ""; }; + D231DC8F2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StackViewController.storyboard; sourceTree = ""; }; + D231DC912C73356D00F3F66C /* StackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; }; + D231DC922C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StepperViewController.storyboard; sourceTree = ""; }; + D231DC942C73356D00F3F66C /* StepperViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperViewController.swift; sourceTree = ""; }; + D231DC952C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SwitchViewController.storyboard; sourceTree = ""; }; + D231DC972C73356D00F3F66C /* SwitchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchViewController.swift; sourceTree = ""; }; + D231DC982C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SymbolViewController.storyboard; sourceTree = ""; }; + D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SymbolViewController.swift; sourceTree = ""; }; + D231DC9B2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextFieldViewController.storyboard; sourceTree = ""; }; + D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldViewController.swift; sourceTree = ""; }; + D231DC9E2C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TextViewController.storyboard; sourceTree = ""; }; + D231DCA02C73356D00F3F66C /* TextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; + D231DCA12C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TintedToolbarViewController.storyboard; sourceTree = ""; }; + D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TintedToolbarViewController.swift; sourceTree = ""; }; + D231DCA62C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/VisualEffectViewController.storyboard; sourceTree = ""; }; + D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualEffectViewController.swift; sourceTree = ""; }; + D231DCA92C73356D00F3F66C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/WebViewController.storyboard; sourceTree = ""; }; + D231DCAB2C73356D00F3F66C /* WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + D231DCF82C7342D500F3F66C /* ModuleBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleBundle.swift; sourceTree = ""; }; + D231DCFA2C735FC200F3F66C /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; + D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticScenario.swift; sourceTree = ""; }; + D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; + D27606982C514F37002D2A14 /* SessionReplayScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayScenario.swift; sourceTree = ""; }; + D276069B2C514F37002D2A14 /* Scenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scenario.swift; sourceTree = ""; }; + D276069D2C514F37002D2A14 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; + D27606B22C526908002D2A14 /* Benchmarks.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Benchmarks.local.xcconfig; sourceTree = ""; }; + D27606B32C526908002D2A14 /* Runner.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Runner.xcconfig; sourceTree = ""; }; + D27606B42C526908002D2A14 /* Synthetics.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Synthetics.xcconfig; sourceTree = ""; }; + D277C84A2C58D3210072343C /* Benchmarks */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Benchmarks; sourceTree = ""; }; + D29F754D2C4AA07E00288638 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D29F754F2C4AA07E00288638 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D29F755D2C4AA08000288638 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D2CA7E862C57F9B800AAB380 /* dd-sdk-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "dd-sdk-ios"; path = ..; sourceTree = ""; }; + D2E60B9F2C732FBB00A18F1C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D231DC2E2C73355800F3F66C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29F754A2C4AA07E00288638 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D27606A92C514F77002D2A14 /* DatadogLogs in Frameworks */, + D27606AF2C514F77002D2A14 /* DatadogTrace in Frameworks */, + D27606AD2C514F77002D2A14 /* DatadogSessionReplay in Frameworks */, + D23DD32D2C58D80C00B90C4C /* DatadogBenchmarks in Frameworks */, + D231DC372C73355800F3F66C /* UIKitCatalog.framework in Frameworks */, + D27606AB2C514F77002D2A14 /* DatadogRUM in Frameworks */, + D27606A72C514F77002D2A14 /* DatadogCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D231DC322C73355800F3F66C /* UIKitCatalog */ = { + isa = PBXGroup; + children = ( + D256FB522C737F5800377260 /* LICENSE */, + D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */, + D231DC422C73356D00F3F66C /* ActivityIndicatorViewController.swift */, + D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */, + D231DC452C73356D00F3F66C /* AlertControllerViewController.swift */, + D231DC472C73356D00F3F66C /* Assets.xcassets */, + D231DC482C73356D00F3F66C /* BaseTableViewController.swift */, + D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */, + D231DC4B2C73356D00F3F66C /* ButtonViewController.swift */, + D231DC4C2C73356D00F3F66C /* ButtonViewController+Configs.swift */, + D231DC4D2C73356D00F3F66C /* CaseElement.swift */, + D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */, + D231DC502C73356D00F3F66C /* ColorPickerViewController.swift */, + D231DC522C73356D00F3F66C /* content.html */, + D231DC542C73356D00F3F66C /* Credits.rtf */, + D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */, + D231DC572C73356D00F3F66C /* CustomPageControlViewController.swift */, + D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */, + D231DC5A2C73356D00F3F66C /* CustomSearchBarViewController.swift */, + D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */, + D231DC5D2C73356D00F3F66C /* CustomToolbarViewController.swift */, + D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */, + D231DC602C73356D00F3F66C /* DatePickerController.swift */, + D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */, + D231DC632C73356D00F3F66C /* DefaultPageControlViewController.swift */, + D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */, + D231DC662C73356D00F3F66C /* DefaultSearchBarViewController.swift */, + D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */, + D231DC692C73356D00F3F66C /* DefaultToolbarViewController.swift */, + D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */, + D231DC6C2C73356D00F3F66C /* FontPickerViewController.swift */, + D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */, + D231DC6F2C73356D00F3F66C /* ImagePickerViewController.swift */, + D231DC712C73356D00F3F66C /* ImageViewController.storyboard */, + D231DC722C73356D00F3F66C /* ImageViewController.swift */, + D231DC782C73356D00F3F66C /* Localizable.strings */, + D231DC7A2C73356D00F3F66C /* Main.storyboard */, + D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */, + D231DC7D2C73356D00F3F66C /* MenuButtonViewController.swift */, + D231DC7E2C73356D00F3F66C /* OutlineViewController.swift */, + D231DC802C73356D00F3F66C /* PickerViewController.storyboard */, + D231DC812C73356D00F3F66C /* PickerViewController.swift */, + D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */, + D231DC842C73356D00F3F66C /* PointerInteractionButtonViewController.swift */, + D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */, + D231DC872C73356D00F3F66C /* ProgressViewController.swift */, + D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */, + D231DC8B2C73356D00F3F66C /* SegmentedControlViewController.swift */, + D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */, + D231DC8E2C73356D00F3F66C /* SliderViewController.swift */, + D231DC902C73356D00F3F66C /* StackViewController.storyboard */, + D231DC912C73356D00F3F66C /* StackViewController.swift */, + D231DC932C73356D00F3F66C /* StepperViewController.storyboard */, + D231DC942C73356D00F3F66C /* StepperViewController.swift */, + D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */, + D231DC972C73356D00F3F66C /* SwitchViewController.swift */, + D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */, + D231DC9A2C73356D00F3F66C /* SymbolViewController.swift */, + D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */, + D231DC9D2C73356D00F3F66C /* TextFieldViewController.swift */, + D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */, + D231DCA02C73356D00F3F66C /* TextViewController.swift */, + D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */, + D231DCA32C73356D00F3F66C /* TintedToolbarViewController.swift */, + D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */, + D231DCA82C73356D00F3F66C /* VisualEffectViewController.swift */, + D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */, + D231DCAB2C73356D00F3F66C /* WebViewController.swift */, + D231DCF82C7342D500F3F66C /* ModuleBundle.swift */, + ); + path = UIKitCatalog; + sourceTree = ""; + }; + D256FB522C737F5800377260 /* LICENSE */ = { + isa = PBXGroup; + children = ( + D231DCFA2C735FC200F3F66C /* LICENSE.txt */, + ); + path = LICENSE; + sourceTree = ""; + }; + D27606992C514F37002D2A14 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D27606982C514F37002D2A14 /* SessionReplayScenario.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; + D276069C2C514F37002D2A14 /* Scenarios */ = { + isa = PBXGroup; + children = ( + D276069B2C514F37002D2A14 /* Scenario.swift */, + D24BFD462C6B916B00AB9604 /* SyntheticScenario.swift */, + D27606992C514F37002D2A14 /* SessionReplay */, + ); + path = Scenarios; + sourceTree = ""; + }; + D27606B52C526908002D2A14 /* xcconfigs */ = { + isa = PBXGroup; + children = ( + D27606B22C526908002D2A14 /* Benchmarks.local.xcconfig */, + D27606B32C526908002D2A14 /* Runner.xcconfig */, + D27606B42C526908002D2A14 /* Synthetics.xcconfig */, + ); + path = xcconfigs; + sourceTree = ""; + }; + D29F75272C4A9EFA00288638 = { + isa = PBXGroup; + children = ( + D2E60B9F2C732FBB00A18F1C /* README.md */, + D27606B52C526908002D2A14 /* xcconfigs */, + D29F754E2C4AA07E00288638 /* Runner */, + D231DC322C73355800F3F66C /* UIKitCatalog */, + D29F75482C4A9F9500288638 /* Frameworks */, + D29F75312C4A9EFA00288638 /* Products */, + ); + sourceTree = ""; + }; + D29F75312C4A9EFA00288638 /* Products */ = { + isa = PBXGroup; + children = ( + D29F754D2C4AA07E00288638 /* Runner.app */, + D231DC312C73355800F3F66C /* UIKitCatalog.framework */, + ); + name = Products; + sourceTree = ""; + }; + D29F75482C4A9F9500288638 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D277C84A2C58D3210072343C /* Benchmarks */, + D2CA7E862C57F9B800AAB380 /* dd-sdk-ios */, + ); + name = Frameworks; + sourceTree = ""; + }; + D29F754E2C4AA07E00288638 /* Runner */ = { + isa = PBXGroup; + children = ( + D29F754F2C4AA07E00288638 /* AppDelegate.swift */, + D276069D2C514F37002D2A14 /* AppConfiguration.swift */, + D24E15F22C776956005AE4E8 /* BenchmarkProfiler.swift */, + D276069C2C514F37002D2A14 /* Scenarios */, + D29F755D2C4AA08000288638 /* Info.plist */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D231DC2C2C73355800F3F66C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D231DC302C73355800F3F66C /* UIKitCatalog */ = { + isa = PBXNativeTarget; + buildConfigurationList = D231DC3C2C73355800F3F66C /* Build configuration list for PBXNativeTarget "UIKitCatalog" */; + buildPhases = ( + D231DC2C2C73355800F3F66C /* Headers */, + D231DC2D2C73355800F3F66C /* Sources */, + D231DC2E2C73355800F3F66C /* Frameworks */, + D231DC2F2C73355800F3F66C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIKitCatalog; + productName = UIKitCatalog; + productReference = D231DC312C73355800F3F66C /* UIKitCatalog.framework */; + productType = "com.apple.product-type.framework"; + }; + D29F754C2C4AA07E00288638 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = D29F75602C4AA08000288638 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + D29F75492C4AA07E00288638 /* Sources */, + D29F754A2C4AA07E00288638 /* Frameworks */, + D29F754B2C4AA07E00288638 /* Resources */, + D29F75872C4AA98F00288638 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D231DC362C73355800F3F66C /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + D27606A62C514F77002D2A14 /* DatadogCore */, + D27606A82C514F77002D2A14 /* DatadogLogs */, + D27606AA2C514F77002D2A14 /* DatadogRUM */, + D27606AC2C514F77002D2A14 /* DatadogSessionReplay */, + D27606AE2C514F77002D2A14 /* DatadogTrace */, + D23DD32C2C58D80C00B90C4C /* DatadogBenchmarks */, + ); + productName = Runner; + productReference = D29F754D2C4AA07E00288638 /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D29F75282C4A9EFA00288638 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + D231DC302C73355800F3F66C = { + CreatedOnToolsVersion = 15.4; + }; + D29F754C2C4AA07E00288638 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = D29F752B2C4A9EFA00288638 /* Build configuration list for PBXProject "BenchmarkTests" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D29F75272C4A9EFA00288638; + packageReferences = ( + ); + productRefGroup = D29F75312C4A9EFA00288638 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D29F754C2C4AA07E00288638 /* Runner */, + D231DC302C73355800F3F66C /* UIKitCatalog */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D231DC2F2C73355800F3F66C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D231DCD62C73356E00F3F66C /* MenuButtonViewController.storyboard in Resources */, + D231DCAF2C73356E00F3F66C /* ActivityIndicatorViewController.storyboard in Resources */, + D231DCDB2C73356E00F3F66C /* PointerInteractionButtonViewController.storyboard in Resources */, + D231DCBE2C73356E00F3F66C /* CustomPageControlViewController.storyboard in Resources */, + D231DCD92C73356E00F3F66C /* PickerViewController.storyboard in Resources */, + D231DCF32C73356E00F3F66C /* VisualEffectViewController.storyboard in Resources */, + D231DCC42C73356E00F3F66C /* DatePickerController.storyboard in Resources */, + D231DCE62C73356E00F3F66C /* StepperViewController.storyboard in Resources */, + D231DCBC2C73356E00F3F66C /* content.html in Resources */, + D231DCD52C73356E00F3F66C /* Main.storyboard in Resources */, + D231DCC62C73356E00F3F66C /* DefaultPageControlViewController.storyboard in Resources */, + D231DCEA2C73356E00F3F66C /* SymbolViewController.storyboard in Resources */, + D231DCEC2C73356E00F3F66C /* TextFieldViewController.storyboard in Resources */, + D231DCCE2C73356E00F3F66C /* ImagePickerViewController.storyboard in Resources */, + D231DCC22C73356E00F3F66C /* CustomToolbarViewController.storyboard in Resources */, + D231DCC02C73356E00F3F66C /* CustomSearchBarViewController.storyboard in Resources */, + D231DCBD2C73356E00F3F66C /* Credits.rtf in Resources */, + D231DCD42C73356E00F3F66C /* Localizable.strings in Resources */, + D231DCE02C73356E00F3F66C /* SegmentedControlViewController.storyboard in Resources */, + D231DCF02C73356E00F3F66C /* TintedToolbarViewController.storyboard in Resources */, + D231DCDD2C73356E00F3F66C /* ProgressViewController.storyboard in Resources */, + D231DCB42C73356E00F3F66C /* Assets.xcassets in Resources */, + D231DCE82C73356E00F3F66C /* SwitchViewController.storyboard in Resources */, + D231DCB12C73356E00F3F66C /* AlertControllerViewController.storyboard in Resources */, + D231DCEE2C73356E00F3F66C /* TextViewController.storyboard in Resources */, + D231DCB62C73356E00F3F66C /* ButtonViewController.storyboard in Resources */, + D231DCBA2C73356E00F3F66C /* ColorPickerViewController.storyboard in Resources */, + D231DCE42C73356E00F3F66C /* StackViewController.storyboard in Resources */, + D231DCCA2C73356E00F3F66C /* DefaultToolbarViewController.storyboard in Resources */, + D231DCE22C73356E00F3F66C /* SliderViewController.storyboard in Resources */, + D231DCCC2C73356E00F3F66C /* FontPickerViewController.storyboard in Resources */, + D231DCC82C73356E00F3F66C /* DefaultSearchBarViewController.storyboard in Resources */, + D231DCF52C73356E00F3F66C /* WebViewController.storyboard in Resources */, + D231DCD02C73356E00F3F66C /* ImageViewController.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29F754B2C4AA07E00288638 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D231DC2D2C73355800F3F66C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D231DCDE2C73356E00F3F66C /* ProgressViewController.swift in Sources */, + D231DCF42C73356E00F3F66C /* VisualEffectViewController.swift in Sources */, + D231DCB02C73356E00F3F66C /* ActivityIndicatorViewController.swift in Sources */, + D231DCE12C73356E00F3F66C /* SegmentedControlViewController.swift in Sources */, + D231DCE72C73356E00F3F66C /* StepperViewController.swift in Sources */, + D231DCC32C73356E00F3F66C /* CustomToolbarViewController.swift in Sources */, + D231DCCF2C73356E00F3F66C /* ImagePickerViewController.swift in Sources */, + D231DCB82C73356E00F3F66C /* ButtonViewController+Configs.swift in Sources */, + D231DCB52C73356E00F3F66C /* BaseTableViewController.swift in Sources */, + D231DCB72C73356E00F3F66C /* ButtonViewController.swift in Sources */, + D231DCF12C73356E00F3F66C /* TintedToolbarViewController.swift in Sources */, + D231DCD72C73356E00F3F66C /* MenuButtonViewController.swift in Sources */, + D231DCB92C73356E00F3F66C /* CaseElement.swift in Sources */, + D231DCF92C7342D500F3F66C /* ModuleBundle.swift in Sources */, + D231DCDC2C73356E00F3F66C /* PointerInteractionButtonViewController.swift in Sources */, + D231DCBB2C73356E00F3F66C /* ColorPickerViewController.swift in Sources */, + D231DCBF2C73356E00F3F66C /* CustomPageControlViewController.swift in Sources */, + D231DCD12C73356E00F3F66C /* ImageViewController.swift in Sources */, + D231DCF62C73356E00F3F66C /* WebViewController.swift in Sources */, + D231DCE32C73356E00F3F66C /* SliderViewController.swift in Sources */, + D231DCE92C73356E00F3F66C /* SwitchViewController.swift in Sources */, + D231DCED2C73356E00F3F66C /* TextFieldViewController.swift in Sources */, + D231DCDA2C73356E00F3F66C /* PickerViewController.swift in Sources */, + D231DCC52C73356E00F3F66C /* DatePickerController.swift in Sources */, + D231DCD82C73356E00F3F66C /* OutlineViewController.swift in Sources */, + D231DCC92C73356E00F3F66C /* DefaultSearchBarViewController.swift in Sources */, + D231DCEF2C73356E00F3F66C /* TextViewController.swift in Sources */, + D231DCC72C73356E00F3F66C /* DefaultPageControlViewController.swift in Sources */, + D231DCB22C73356E00F3F66C /* AlertControllerViewController.swift in Sources */, + D231DCCD2C73356E00F3F66C /* FontPickerViewController.swift in Sources */, + D231DCC12C73356E00F3F66C /* CustomSearchBarViewController.swift in Sources */, + D231DCEB2C73356E00F3F66C /* SymbolViewController.swift in Sources */, + D231DCE52C73356E00F3F66C /* StackViewController.swift in Sources */, + D231DCCB2C73356E00F3F66C /* DefaultToolbarViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29F75492C4AA07E00288638 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D27606A42C514F37002D2A14 /* AppConfiguration.swift in Sources */, + D29F75502C4AA07E00288638 /* AppDelegate.swift in Sources */, + D27606A12C514F37002D2A14 /* SessionReplayScenario.swift in Sources */, + D24E15F32C776956005AE4E8 /* BenchmarkProfiler.swift in Sources */, + D24BFD472C6B916B00AB9604 /* SyntheticScenario.swift in Sources */, + D27606A32C514F37002D2A14 /* Scenario.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + D231DC362C73355800F3F66C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D231DC302C73355800F3F66C /* UIKitCatalog */; + targetProxy = D231DC352C73355800F3F66C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + D231DC412C73356D00F3F66C /* ActivityIndicatorViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC402C73356D00F3F66C /* Base */, + ); + name = ActivityIndicatorViewController.storyboard; + sourceTree = ""; + }; + D231DC442C73356D00F3F66C /* AlertControllerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC432C73356D00F3F66C /* Base */, + ); + name = AlertControllerViewController.storyboard; + sourceTree = ""; + }; + D231DC4A2C73356D00F3F66C /* ButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC492C73356D00F3F66C /* Base */, + ); + name = ButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC4F2C73356D00F3F66C /* ColorPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC4E2C73356D00F3F66C /* Base */, + ); + name = ColorPickerViewController.storyboard; + sourceTree = ""; + }; + D231DC522C73356D00F3F66C /* content.html */ = { + isa = PBXVariantGroup; + children = ( + D231DC512C73356D00F3F66C /* Base */, + ); + name = content.html; + sourceTree = ""; + }; + D231DC542C73356D00F3F66C /* Credits.rtf */ = { + isa = PBXVariantGroup; + children = ( + D231DC532C73356D00F3F66C /* Base */, + ); + name = Credits.rtf; + sourceTree = ""; + }; + D231DC562C73356D00F3F66C /* CustomPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC552C73356D00F3F66C /* Base */, + ); + name = CustomPageControlViewController.storyboard; + sourceTree = ""; + }; + D231DC592C73356D00F3F66C /* CustomSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC582C73356D00F3F66C /* Base */, + ); + name = CustomSearchBarViewController.storyboard; + sourceTree = ""; + }; + D231DC5C2C73356D00F3F66C /* CustomToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC5B2C73356D00F3F66C /* Base */, + ); + name = CustomToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DC5F2C73356D00F3F66C /* DatePickerController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC5E2C73356D00F3F66C /* Base */, + ); + name = DatePickerController.storyboard; + sourceTree = ""; + }; + D231DC622C73356D00F3F66C /* DefaultPageControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC612C73356D00F3F66C /* Base */, + ); + name = DefaultPageControlViewController.storyboard; + sourceTree = ""; + }; + D231DC652C73356D00F3F66C /* DefaultSearchBarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC642C73356D00F3F66C /* Base */, + ); + name = DefaultSearchBarViewController.storyboard; + sourceTree = ""; + }; + D231DC682C73356D00F3F66C /* DefaultToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC672C73356D00F3F66C /* Base */, + ); + name = DefaultToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DC6B2C73356D00F3F66C /* FontPickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC6A2C73356D00F3F66C /* Base */, + ); + name = FontPickerViewController.storyboard; + sourceTree = ""; + }; + D231DC6E2C73356D00F3F66C /* ImagePickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC6D2C73356D00F3F66C /* Base */, + ); + name = ImagePickerViewController.storyboard; + sourceTree = ""; + }; + D231DC712C73356D00F3F66C /* ImageViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC702C73356D00F3F66C /* Base */, + ); + name = ImageViewController.storyboard; + sourceTree = ""; + }; + D231DC782C73356D00F3F66C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D231DC772C73356D00F3F66C /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D231DC7A2C73356D00F3F66C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC792C73356D00F3F66C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D231DC7C2C73356D00F3F66C /* MenuButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC7B2C73356D00F3F66C /* Base */, + ); + name = MenuButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC802C73356D00F3F66C /* PickerViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC7F2C73356D00F3F66C /* Base */, + ); + name = PickerViewController.storyboard; + sourceTree = ""; + }; + D231DC832C73356D00F3F66C /* PointerInteractionButtonViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC822C73356D00F3F66C /* Base */, + ); + name = PointerInteractionButtonViewController.storyboard; + sourceTree = ""; + }; + D231DC862C73356D00F3F66C /* ProgressViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC852C73356D00F3F66C /* Base */, + ); + name = ProgressViewController.storyboard; + sourceTree = ""; + }; + D231DC8A2C73356D00F3F66C /* SegmentedControlViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC892C73356D00F3F66C /* Base */, + ); + name = SegmentedControlViewController.storyboard; + sourceTree = ""; + }; + D231DC8D2C73356D00F3F66C /* SliderViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC8C2C73356D00F3F66C /* Base */, + ); + name = SliderViewController.storyboard; + sourceTree = ""; + }; + D231DC902C73356D00F3F66C /* StackViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC8F2C73356D00F3F66C /* Base */, + ); + name = StackViewController.storyboard; + sourceTree = ""; + }; + D231DC932C73356D00F3F66C /* StepperViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC922C73356D00F3F66C /* Base */, + ); + name = StepperViewController.storyboard; + sourceTree = ""; + }; + D231DC962C73356D00F3F66C /* SwitchViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC952C73356D00F3F66C /* Base */, + ); + name = SwitchViewController.storyboard; + sourceTree = ""; + }; + D231DC992C73356D00F3F66C /* SymbolViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC982C73356D00F3F66C /* Base */, + ); + name = SymbolViewController.storyboard; + sourceTree = ""; + }; + D231DC9C2C73356D00F3F66C /* TextFieldViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC9B2C73356D00F3F66C /* Base */, + ); + name = TextFieldViewController.storyboard; + sourceTree = ""; + }; + D231DC9F2C73356D00F3F66C /* TextViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DC9E2C73356D00F3F66C /* Base */, + ); + name = TextViewController.storyboard; + sourceTree = ""; + }; + D231DCA22C73356D00F3F66C /* TintedToolbarViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA12C73356D00F3F66C /* Base */, + ); + name = TintedToolbarViewController.storyboard; + sourceTree = ""; + }; + D231DCA72C73356D00F3F66C /* VisualEffectViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA62C73356D00F3F66C /* Base */, + ); + name = VisualEffectViewController.storyboard; + sourceTree = ""; + }; + D231DCAA2C73356D00F3F66C /* WebViewController.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D231DCA92C73356D00F3F66C /* Base */, + ); + name = WebViewController.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D231DC392C73355800F3F66C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + D231DC3A2C73355800F3F66C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + D231DC3B2C73355800F3F66C /* Synthetics */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.UIKitCatalog; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Synthetics; + }; + D27606B62C526925002D2A14 /* Synthetics */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Synthetics; + }; + D27606B72C526925002D2A14 /* Synthetics */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D27606B42C526908002D2A14 /* Synthetics.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Synthetics; + }; + D29F75422C4A9EFB00288638 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + D29F75432C4A9EFB00288638 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + D29F755E2C4AA08000288638 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D27606B32C526908002D2A14 /* Runner.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D29F755F2C4AA08000288638 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D27606B32C526908002D2A14 /* Runner.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Datadog Benchmark Runner"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.benchmarks.Runner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D231DC3C2C73355800F3F66C /* Build configuration list for PBXNativeTarget "UIKitCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D231DC392C73355800F3F66C /* Debug */, + D231DC3A2C73355800F3F66C /* Release */, + D231DC3B2C73355800F3F66C /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D29F752B2C4A9EFA00288638 /* Build configuration list for PBXProject "BenchmarkTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D29F75422C4A9EFB00288638 /* Debug */, + D29F75432C4A9EFB00288638 /* Release */, + D27606B62C526925002D2A14 /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D29F75602C4AA08000288638 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D29F755E2C4AA08000288638 /* Debug */, + D29F755F2C4AA08000288638 /* Release */, + D27606B72C526925002D2A14 /* Synthetics */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + D23DD32C2C58D80C00B90C4C /* DatadogBenchmarks */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogBenchmarks; + }; + D27606A62C514F77002D2A14 /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogCore; + }; + D27606A82C514F77002D2A14 /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogLogs; + }; + D27606AA2C514F77002D2A14 /* DatadogRUM */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogRUM; + }; + D27606AC2C514F77002D2A14 /* DatadogSessionReplay */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogSessionReplay; + }; + D27606AE2C514F77002D2A14 /* DatadogTrace */ = { + isa = XCSwiftPackageProductDependency; + productName = DatadogTrace; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = D29F75282C4A9EFA00288638 /* Project object */; +} diff --git a/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from dependency-manager-tests/carthage/CTProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 57% rename from dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme rename to BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9e772b47d7..73079a7d00 100644 --- a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/App macOS.xcscheme +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + BlueprintIdentifier = "D29F754C2C4AA07E00288638" + BuildableName = "Runner.app" + BlueprintName = "Runner" + ReferencedContainer = "container:BenchmarkTests.xcodeproj"> @@ -29,30 +29,6 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> - - - - - - - - - - + BlueprintIdentifier = "D29F754C2C4AA07E00288638" + BuildableName = "Runner.app" + BlueprintName = "Runner" + ReferencedContainer = "container:BenchmarkTests.xcodeproj"> + + + + + + + BlueprintIdentifier = "D29F754C2C4AA07E00288638" + BuildableName = "Runner.app" + BlueprintName = "Runner" + ReferencedContainer = "container:BenchmarkTests.xcodeproj"> diff --git a/BenchmarkTests/Benchmarks/Package.swift b/BenchmarkTests/Benchmarks/Package.swift new file mode 100644 index 0000000000..7484f7b117 --- /dev/null +++ b/BenchmarkTests/Benchmarks/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import Foundation + +let package = Package( + name: "DatadogBenchmarks", + products: [ + .library( + name: "DatadogBenchmarks", + targets: ["DatadogBenchmarks"] + ) + ] +) + +func addOpenTelemetryDependency(_ version: Version) { + // The project must be open with the 'OTEL_SWIFT' env variable. + // Please run 'make benchmark-tests-open' from the root directory. + // + // Note: Carthage will still try to resolve dependencies of Xcode projects in + // sub directories, in this case the project will depend on the default + // 'DataDog/opentelemetry-swift-packages' depedency. + if ProcessInfo.processInfo.environment["OTEL_SWIFT"] != nil { + package.dependencies = [ + .package(url: "https://github.com/open-telemetry/opentelemetry-swift", exact: version) + ] + + package.targets = [ + .target( + name: "DatadogBenchmarks", + dependencies: [ + .product(name: "OpenTelemetryApi", package: "opentelemetry-swift"), + .product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"), + .product(name: "DatadogExporter", package: "opentelemetry-swift") + ], + swiftSettings: [.define("OTEL_SWIFT")] + ) + ] + } else { + package.dependencies = [ + .package(url: "https://github.com/DataDog/opentelemetry-swift-packages", exact: version) + ] + + package.targets = [ + .target( + name: "DatadogBenchmarks", + dependencies: [ + .product(name: "OpenTelemetryApi", package: "opentelemetry-swift-packages") + ], + swiftSettings: [.define("OTEL_API")] + ) + ] + } +} + +addOpenTelemetryDependency("1.6.0") diff --git a/BenchmarkTests/Benchmarks/Sources/Benchmarks.swift b/BenchmarkTests/Benchmarks/Sources/Benchmarks.swift new file mode 100644 index 0000000000..2a45d5ebb2 --- /dev/null +++ b/BenchmarkTests/Benchmarks/Sources/Benchmarks.swift @@ -0,0 +1,171 @@ +/* + * 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. + */ + +#if OTEL_API +#error("Benchmarks depends on opentelemetry-swift. Please open the project with 'make benchmark-tests-open'.") +#endif + +import Foundation +import OpenTelemetryApi +import OpenTelemetrySdk +import DatadogExporter + +let instrumentationName = "benchmarks" +let instrumentationVersion = "1.0.0" + +/// Benchmark entrypoint to configure opentelemetry with metrics meters +/// and tracer. +public enum Benchmarks { + /// Configuration of the Benchmarks library. + public struct Configuration { + /// Context of Benchmarks measures. + /// The context properties will be added to metrics as tags. + public struct Context { + var applicationIdentifier: String + var applicationName: String + var applicationVersion: String + var sdkVersion: String + var deviceModel: String + var osName: String + var osVersion: String + var run: String + var scenario: String + var branch: String + + public init( + applicationIdentifier: String, + applicationName: String, + applicationVersion: String, + sdkVersion: String, + deviceModel: String, + osName: String, + osVersion: String, + run: String, + scenario: String, + branch: String + ) { + self.applicationIdentifier = applicationIdentifier + self.applicationName = applicationName + self.applicationVersion = applicationVersion + self.sdkVersion = sdkVersion + self.deviceModel = deviceModel + self.osName = osName + self.osVersion = osVersion + self.run = run + self.scenario = scenario + self.branch = branch + } + } + + var clientToken: String + var apiKey: String + var context: Context + + public init( + clientToken: String, + apiKey: String, + context: Context + ) { + self.clientToken = clientToken + self.apiKey = apiKey + self.context = context + } + } + + /// Configure OpenTelemetry metrics meter and start measuring Memory. + /// + /// - Parameter configuration: The Benchmark configuration. + public static func enableMetrics(with configuration: Configuration) { + let metricExporter = MetricExporter( + configuration: MetricExporter.Configuration( + apiKey: configuration.apiKey, + version: instrumentationVersion + ) + ) + + let meterProvider = MeterProviderBuilder() + .with(pushInterval: 10) + .with(processor: MetricProcessorSdk()) + .with(exporter: metricExporter) + .with(resource: Resource()) + .build() + + let meter = meterProvider.get( + instrumentationName: instrumentationName, + instrumentationVersion: instrumentationVersion + ) + + let labels = [ + "device_model": configuration.context.deviceModel, + "os": configuration.context.osName, + "os_version": configuration.context.osVersion, + "run": configuration.context.run, + "scenario": configuration.context.scenario, + "application_id": configuration.context.applicationIdentifier, + "sdk_version": configuration.context.sdkVersion, + "branch": configuration.context.branch, + ] + + let queue = DispatchQueue(label: "com.datadoghq.benchmarks.metrics", qos: .utility) + + let memory = Memory(queue: queue) + _ = meter.createDoubleObservableGauge(name: "ios.benchmark.memory") { metric in + // report the maximum memory footprint that was recorded during push interval + if let value = memory.aggregation?.max { + metric.observe(value: value, labels: labels) + } + + memory.reset() + } + + let cpu = CPU(queue: queue) + _ = meter.createDoubleObservableGauge(name: "ios.benchmark.cpu") { metric in + // report the average cpu usage that was recorded during push interval + if let value = cpu.aggregation?.avg { + metric.observe(value: value, labels: labels) + } + + cpu.reset() + } + + let fps = FPS() + _ = meter.createIntObservableGauge(name: "ios.benchmark.fps.min") { metric in + // report the minimum frame rate that was recorded during push interval + if let value = fps.aggregation?.min { + metric.observe(value: value, labels: labels) + } + + fps.reset() + } + + OpenTelemetry.registerMeterProvider(meterProvider: meterProvider) + } + + /// Configure and register a OpenTelemetry Tracer. + /// + /// - Parameter configuration: The Benchmark configuration. + public static func enableTracer(with configuration: Configuration) { + let exporterConfiguration = ExporterConfiguration( + serviceName: configuration.context.applicationIdentifier, + resource: "Benchmark Tracer", + applicationName: configuration.context.applicationName, + applicationVersion: configuration.context.applicationVersion, + environment: "benchmarks", + apiKey: configuration.apiKey, + endpoint: .us1, + uploadCondition: { true } + ) + + let exporter = try! DatadogExporter(config: exporterConfiguration) + let processor = SimpleSpanProcessor(spanExporter: exporter) + + let provider = TracerProviderBuilder() + .add(spanProcessor: processor) + .build() + + OpenTelemetry.registerTracerProvider(tracerProvider: provider) + } +} diff --git a/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift b/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift new file mode 100644 index 0000000000..98429b227d --- /dev/null +++ b/BenchmarkTests/Benchmarks/Sources/MetricExporter.swift @@ -0,0 +1,162 @@ +/* + * 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 OpenTelemetrySdk + +enum MetricExporterError: Error { + case unsupportedMetric(aggregation: AggregationType, dataType: Any.Type) +} + +/// Replacement of otel `DatadogExporter` for metrics. +/// +/// This version does not store data to disk, it uploads to the intake directly. +/// Additionally, it does not crash. +final class MetricExporter: OpenTelemetrySdk.MetricExporter { + struct Configuration { + let apiKey: String + let version: String + } + + /// The type of metric. The available types are 0 (unspecified), 1 (count), 2 (rate), and 3 (gauge). Allowed enum values: 0,1,2,3 + enum MetricType: Int, Codable { + case unspecified = 0 + case count = 1 + case rate = 2 + case gauge = 3 + } + + /// https://docs.datadoghq.com/api/latest/metrics/#submit-metrics + internal struct Serie: Codable { + struct Point: Codable { + let timestamp: Int64 + let value: Double + } + + struct Resource: Codable { + let name: String + let type: String + } + + let type: MetricType + let interval: Int64? + let metric: String + let unit: String? + let points: [Point] + let resources: [Resource] + let tags: [String] + } + + let session: URLSession + let encoder = JSONEncoder() + let configuration: Configuration + + // swiftlint:disable force_unwrapping + let intake = URL(string: "https://api.datadoghq.com/api/v2/series")! + let prefix = "{ \"series\": [".data(using: .utf8)! + let separator = ",".data(using: .utf8)! + let suffix = "]}".data(using: .utf8)! + // swiftlint:enable force_unwrapping + + required init(configuration: Configuration) { + let sessionConfiguration: URLSessionConfiguration = .ephemeral + sessionConfiguration.urlCache = nil + self.session = URLSession(configuration: sessionConfiguration) + self.configuration = configuration + } + + func export(metrics: [Metric], shouldCancel: (() -> Bool)?) -> MetricExporterResultCode { + do { + let series = try metrics.map(transform) + try submit(series: series) + return.success + } catch { + return .failureNotRetryable + } + } + + /// Transforms otel `Metric` to Datadog `serie`. + /// + /// - Parameter metric: The otel metric + /// - Returns: The timeserie. + func transform(_ metric: Metric) throws -> Serie { + var tags: Set = [] + + let points: [Serie.Point] = try metric.data.map { data in + let timestamp = Int64(data.timestamp.timeIntervalSince1970) + + data.labels.forEach { tags.insert("\($0):\($1)") } + + switch data { + case let data as SumData: + return Serie.Point(timestamp: timestamp, value: data.sum) + case let data as SumData: + return Serie.Point(timestamp: timestamp, value: Double(data.sum)) + case let data as SummaryData: + return Serie.Point(timestamp: timestamp, value: data.sum) + case let data as SummaryData: + return Serie.Point(timestamp: timestamp, value: Double(data.sum)) +// case let data as HistogramData: +// return Serie.Point(timestamp: timestamp, value: Double(data.sum)) +// case let data as HistogramData: +// return Serie.Point(timestamp: timestamp, value: data.sum) + default: + throw MetricExporterError.unsupportedMetric( + aggregation: metric.aggregationType, + dataType: type(of: data) + ) + } + } + + return Serie( + type: MetricType(metric.aggregationType), + interval: nil, + metric: metric.name, + unit: nil, + points: points, + resources: [], + tags: Array(tags) + ) + } + + /// Submit timeseries to the Metrics intake. + /// + /// - Parameter series: The timeseries. + func submit(series: [Serie]) throws { + var data = try series.reduce(Data()) { data, serie in + try data + encoder.encode(serie) + separator + } + + // remove last separator + data.removeLast(separator.count) + + var request = URLRequest(url: intake) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ + "Content-Type": "application/json", + "DD-API-KEY": configuration.apiKey, + "DD-EVP-ORIGIN": "ios", + "DD-EVP-ORIGIN-VERSION": configuration.version, + "DD-REQUEST-ID": UUID().uuidString, + ] + + request.httpBody = prefix + data + suffix + session.dataTask(with: request).resume() + } +} + +private extension MetricExporter.MetricType { + init(_ type: OpenTelemetrySdk.AggregationType) { + switch type { + case .doubleSum, .intSum: + self = .count + case .intGauge, .doubleGauge: + self = .gauge + case .doubleSummary, .intSummary, .doubleHistogram, .intHistogram: + self = .unspecified + } + } +} diff --git a/BenchmarkTests/Benchmarks/Sources/Metrics.swift b/BenchmarkTests/Benchmarks/Sources/Metrics.swift new file mode 100644 index 0000000000..9c6666546c --- /dev/null +++ b/BenchmarkTests/Benchmarks/Sources/Metrics.swift @@ -0,0 +1,275 @@ +/* + * 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 QuartzCore + +// The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too +// complex for the Swift C importer, so we have to define them ourselves. +let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) +let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(MemoryLayout.offset(of: \task_vm_info_data_t.min_address)! / MemoryLayout.size) + +internal enum MachError: Error { + case task_info(return: kern_return_t) + case task_threads(return: kern_return_t) + case thread_info(return: kern_return_t) +} + +/// Aggregate metric values and compute `min`, `max`, `sum`, `avg`, and `count`. +internal class MetricAggregator where T: Numeric { + internal struct Aggregation { + let min: T + let max: T + let sum: T + let count: Int + let avg: Double + } + + private var mutex = pthread_mutex_t() + private var _aggregation: Aggregation? + + var aggregation: Aggregation? { + pthread_mutex_lock(&mutex) + defer { pthread_mutex_unlock(&mutex) } + return _aggregation + } + + /// Resets the minimum frame rate to `nil`. + func reset() { + pthread_mutex_lock(&mutex) + _aggregation = nil + pthread_mutex_unlock(&mutex) + } + + deinit { + pthread_mutex_destroy(&mutex) + } +} + +extension MetricAggregator where T: BinaryInteger { + /// Records a `BinaryInteger` value. + /// + /// - Parameter value: The value to record. + func record(value: T) { + pthread_mutex_lock(&mutex) + _aggregation = _aggregation.map { + let sum = $0.sum + value + let count = $0.count + 1 + return Aggregation( + min: Swift.min($0.min, value), + max: Swift.max($0.max, value), + sum: sum, + count: count, + avg: Double(sum) / Double(count) + ) + } ?? Aggregation(min: value, max: value, sum: value, count: 1, avg: Double(value)) + pthread_mutex_unlock(&mutex) + } +} + +extension MetricAggregator where T: BinaryFloatingPoint { + /// Records a `BinaryFloatingPoint` value. + /// + /// - Parameter value: The value to record. + func record(value: T) { + pthread_mutex_lock(&mutex) + _aggregation = _aggregation.map { + let sum = $0.sum + value + let count = $0.count + 1 + return Aggregation( + min: Swift.min($0.min, value), + max: Swift.max($0.max, value), + sum: sum, + count: count, + avg: Double(sum) / Double(count) + ) + } ?? Aggregation(min: value, max: value, sum: value, count: 1, avg: Double(value)) + pthread_mutex_unlock(&mutex) + } +} + +/// Collect Memory footprint metric. +/// +/// Based on a timer, the `Memory` aggregator will periodically record the memory footprint. +internal final class Memory: MetricAggregator { + /// Dispatch source object for monitoring timer events. + private let timer: DispatchSourceTimer + + /// Create a `Memory` aggregator to periodically record the memory footprint on the + /// provided queue. + /// + /// By default, the timer is scheduled with 100 ms interval with 10 ms leeway. + /// + /// - Parameters: + /// - queue: The queue on which to execute the timer handler. + /// - interval: The timer interval, default to 100 ms. + /// - leeway: The timer leeway, default to 10 ms. + required init( + queue: DispatchQueue, + every interval: DispatchTimeInterval = .milliseconds(100), + leeway: DispatchTimeInterval = .milliseconds(10) + ) { + timer = DispatchSource.makeTimerSource(queue: queue) + super.init() + + timer.setEventHandler { [weak self] in + guard let self, let footprint = try? self.footprint() else { + return + } + + self.record(value: footprint) + } + + timer.schedule(deadline: .now(), repeating: interval, leeway: leeway) + timer.activate() + } + + deinit { + timer.cancel() + } + + /// Collects single sample of current memory footprint. + /// + /// The computation is based on https://developer.apple.com/forums/thread/105088 + /// It leverages recommended `phys_footprint` value, which returns values that are close to Xcode's _Memory Use_ + /// gauge and _Allocations Instrument_. + /// + /// - Returns: Current memory footprint in bytes, `throws` if failed to read. + private func footprint() throws -> Double { + var info = task_vm_info_data_t() + var count = TASK_VM_INFO_COUNT + let kr = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) + } + } + + guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else { + throw MachError.task_info(return: kr) + } + + return Double(info.phys_footprint) + } +} + +/// Collect CPU usage metric. +/// +/// Based on a timer, the `CPU` aggregator will periodically record the CPU usage. +internal final class CPU: MetricAggregator { + /// Dispatch source object for monitoring timer events. + private let timer: DispatchSourceTimer + + /// Create a `CPU` aggregator to periodically record the CPU usage on the + /// provided queue. + /// + /// By default, the timer is scheduled with 100 ms interval with 10 ms leeway. + /// + /// - Parameters: + /// - queue: The queue on which to execute the timer handler. + /// - interval: The timer interval, default to 100 ms. + /// - leeway: The timer leeway, default to 10 ms. + init( + queue: DispatchQueue, + every interval: DispatchTimeInterval = .milliseconds(100), + leeway: DispatchTimeInterval = .milliseconds(10) + ) { + self.timer = DispatchSource.makeTimerSource(queue: queue) + super.init() + + timer.setEventHandler { [weak self] in + guard let self, let usage = try? self.usage() else { + return + } + + self.record(value: usage) + } + + timer.schedule(deadline: .now(), repeating: interval, leeway: leeway) + timer.activate() + } + + deinit { + timer.cancel() + } + + /// Collect single sample of current cpu usage. + /// + /// The computation is based on https://gist.github.com/hisui/10004131#file-cpu-usage-cpp + /// It reads the `cpu_usage` from all thread to compute the application usage percentage. + /// + /// - Returns: The cpu usage of all threads. + private func usage() throws -> Double { + var threads_list: thread_act_array_t? + var threads_count = mach_msg_type_number_t() + let kr = withUnsafeMutablePointer(to: &threads_list) { + $0.withMemoryRebound(to: thread_act_array_t?.self, capacity: 1) { + task_threads(mach_task_self_, $0, &threads_count) + } + } + + guard kr == KERN_SUCCESS, let threads_list = threads_list else { + throw MachError.task_threads(return: kr) + } + + defer { + vm_deallocate(mach_task_self_, vm_address_t(bitPattern: threads_list), vm_size_t(Int(threads_count) * MemoryLayout.stride)) + } + + return try (0.. { + private class CADisplayLinker { + weak var fps: FPS? + + init() { } + + @objc + func tick(link: CADisplayLink) { + guard let fps else { + return + } + + let rate = 1 / (link.targetTimestamp - link.timestamp) + fps.record(value: lround(rate)) + } + } + + private var displayLink: CADisplayLink + + override init() { + let linker = CADisplayLinker() + displayLink = CADisplayLink(target: linker, selector: #selector(CADisplayLinker.tick(link:))) + super.init() + + linker.fps = self + displayLink.add(to: RunLoop.main, forMode: .common) + } + + deinit { + displayLink.invalidate() + } +} diff --git a/BenchmarkTests/Makefile b/BenchmarkTests/Makefile new file mode 100644 index 0000000000..e0478c0f52 --- /dev/null +++ b/BenchmarkTests/Makefile @@ -0,0 +1,73 @@ +.PHONY: clean archive export upload + +REPO_ROOT := ../ +include ../tools/utils/common.mk + +BUILD_DIR := .build +ARCHIVE_PATH := $(BUILD_DIR)/Runner.xcarchive +IPA_PATH := $(ARTIFACTS_PATH)/Runner.ipa + +clean: + @$(ECHO_SUBTITLE2) "make clean" + rm -rf "$(BUILD_DIR)" +ifdef ARTIFACTS_PATH + rm -rf "$(IPA_PATH)" +endif + +build: + @$(ECHO_SUBTITLE2) "make build" + set -eo pipefail; \ + DD_BENCHMARK=1 OTEL_SWIFT=1 xcodebuild \ + -project BenchmarkTests.xcodeproj \ + -scheme Runner \ + -sdk iphonesimulator \ + -configuration Release \ + -destination generic/platform=iOS\ Simulator \ + | xcbeautify + @$(ECHO_SUCCESS) "BenchmarkTests compiles" + +archive: + @:$(eval VERSION ?= $(CURRENT_GIT_COMMIT_SHORT)) + @$(ECHO_SUBTITLE2) "make archive VERSION='$(VERSION)'" + @xcrun agvtool new-version "$(VERSION)" + set -eo pipefail; \ + DD_BENCHMARK=1 OTEL_SWIFT=1 xcodebuild \ + -project BenchmarkTests.xcodeproj \ + -scheme Runner \ + -sdk iphoneos \ + -configuration Synthetics \ + -destination generic/platform=iOS \ + -archivePath $(ARCHIVE_PATH) \ + archive | xcbeautify + @$(ECHO_SUCCESS) "Archive ready in '$(ARCHIVE_PATH)'" + +export: + @$(call require_param,ARTIFACTS_PATH) + @:$(eval VERSION ?= $(CURRENT_GIT_COMMIT_SHORT)) + @$(ECHO_SUBTITLE2) "make export VERSION='$(VERSION)' ARTIFACTS_PATH='$(ARTIFACTS_PATH)'" + set -o pipefaill; \ + xcodebuild -exportArchive \ + -archivePath $(ARCHIVE_PATH) \ + -exportOptionsPlist exportOptions.plist \ + -exportPath $(BUILD_DIR) \ + | xcbeautify + mkdir -p "$(ARTIFACTS_PATH)" + cp -v "$(BUILD_DIR)/Runner.ipa" "$(IPA_PATH)" + @$(ECHO_SUCCESS) "IPA exported to '$(IPA_PATH)'" + +upload: + @$(call require_param,ARTIFACTS_PATH) + @$(call require_param,DATADOG_API_KEY) + @$(call require_param,DATADOG_APP_KEY) + @$(call require_param,S8S_APPLICATION_ID) + @:$(eval VERSION ?= $(CURRENT_GIT_COMMIT_SHORT)) + @$(ECHO_SUBTITLE2) "make upload VERSION='$(VERSION)' ARTIFACTS_PATH='$(ARTIFACTS_PATH)'" + datadog-ci synthetics upload-application \ + --mobileApp "$(IPA_PATH)" \ + --mobileApplicationId "${S8S_APPLICATION_ID}" \ + --versionName "$(VERSION)" \ + --latest + +open: + @$(ECHO_SUBTITLE2) "make open" + @open --env DD_BENCHMARK --env OTEL_SWIFT --new BenchmarkTests.xcodeproj diff --git a/BenchmarkTests/README.md b/BenchmarkTests/README.md new file mode 100644 index 0000000000..38c1e6fcbc --- /dev/null +++ b/BenchmarkTests/README.md @@ -0,0 +1,79 @@ +# Benchmark Tests + +[Synthetics for Mobile](https://docs.datadoghq.com/mobile_app_testing/) runs Benchmark test scenarios to collect metrics of the SDK performances. + + +## CI + +CI continuously builds, signs, and uploads a runner application to Synthetics, which runs predefined tests. + +### Build + +Before building the application, make sure the `BenchmarkTests/xcconfigs/Benchmark.local.xcconfig` configuration file is present and contains the `Mobile - Integration Org` client token, RUM application ID, and API Key. These values are sensitive and must be securely stored. + +```ini +CLIENT_TOKEN= +RUM_APPLICATION_ID= +API_KEY= +``` + +### Sign + +To sign the runner application, the certificate and provision profile defined in [Synthetics.xcconfig](xcconfigs/Synthetics.xcconfig) and in [exportOptions.plist](exportOptions.plist) needs to be installed on the build machine. The certificate and profile are sensitive files and must be securely stored. Make sure to update both files when updating the certificate and provisioning profile, otherwise signing fails. + +> [!NOTE] +> Certificate & Provisioning Profile are also available through the [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi). But we don't have the tooling in place. + +### Upload + +The application version (build number) is set to the commit SHA of the current job, and the build is uploaded to Synthetics using the [datadog-ci](https://github.com/DataDog/datadog-ci) CLI. This step expects environment variables to authenticate with the `Mobile - Integration Org`: + +```bash +export DATADOG_API_KEY= +export DATADOG_APP_KEY= +export S8S_APPLICATION_ID= +``` + +## Development + +Each scenario is independent and can be considered as an app within the runner. + +### Create a scenario + +A scenario must comply with the [`Scenario`](Runner/Scenarios/Scenario.swift) protocol. Upon start, a scenario initializes the SDK, enables features, and returns a root view-controller. + +Here is a simple example of a scenario using Logs: +```swift +import Foundation +import UIKit + +import DatadogCore +import DatadogLogs + +struct LogsScenario: Scenario { + + /// The initial view-controller of the scenario + let initialViewController: UIViewController = LoggerViewController() + + /// Start instrumenting the application by enabling the Datadog SDK and + /// its Features. + /// + /// - Parameter info: The application information to use during SDK + /// initialisation. + func instrument(with info: AppInfo) { + + Datadog.initialize( + with: .benchmark(info: info), // SDK init with the benchmark configuration + trackingConsent: .granted + ) + + Logs.enable() + } +} +``` + +Add the test to the [`SyntheticScenario`](Runner/Scenarios/SyntheticScenario.swift#L12) object so it can be selected by setting the `BENCHMARK_SCENARIO` environment variable. + +### Synthetics Configuration + +Please refer to [Confluence page (internal)](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3981476482/Benchmarks+iOS) \ No newline at end of file diff --git a/BenchmarkTests/Runner/AppConfiguration.swift b/BenchmarkTests/Runner/AppConfiguration.swift new file mode 100644 index 0000000000..be251360c6 --- /dev/null +++ b/BenchmarkTests/Runner/AppConfiguration.swift @@ -0,0 +1,76 @@ +/* + * 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 DatadogInternal +import DatadogCore + +/// Application info reads configuration from `Info.plist`. +/// +/// The expected format is as follow: +/// +/// +/// DatadogConfiguration +/// +/// ClientToken +/// $(CLIENT_TOKEN) +/// ApplicationID +/// $(RUM_APPLICATION_ID) +/// ApiKey +/// $(API_KEY) +/// Environment +/// $(DD_ENV) +/// Site +/// $(DD_SITE) +/// +/// +struct AppInfo: Decodable { + let clientToken: String + let applicationID: String + let apiKey: String + let site: DatadogSite + let env: String + + enum CodingKeys: String, CodingKey { + case clientToken = "ClientToken" + case applicationID = "ApplicationID" + case apiKey = "ApiKey" + case site = "Site" + case env = "Environment" + } +} + +extension AppInfo { + init(bundle: Bundle = .main) throws { + let decoder = AnyDecoder() + let obj = bundle.object(forInfoDictionaryKey: "DatadogConfiguration") + self = try decoder.decode(from: obj) + } +} + +extension AppInfo { + static var empty: Self { + .init( + clientToken: "", + applicationID: "", + apiKey: "", + site: .us1, + env: "benchmarks" + ) + } +} + +extension DatadogSite: Decodable {} + +extension Datadog.Configuration { + static func benchmark(info: AppInfo) -> Self { + .init( + clientToken: info.clientToken, + env: info.env, + site: info.site + ) + } +} diff --git a/BenchmarkTests/Runner/AppDelegate.swift b/BenchmarkTests/Runner/AppDelegate.swift new file mode 100644 index 0000000000..4ba37aa573 --- /dev/null +++ b/BenchmarkTests/Runner/AppDelegate.swift @@ -0,0 +1,88 @@ +/* + * 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 UIKit +import DatadogInternal +import DatadogBenchmarks + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + guard let scenario = SyntheticScenario() else { + return false + } + + let run = SyntheticRun() + let applicationInfo = try! AppInfo() // crash if info are missing or malformed + + switch run { + case .baseline, .instrumented: + // measure metrics during baseline and metrics runs + Benchmarks.enableMetrics( + with: Benchmarks.Configuration( + info: applicationInfo, + scenario: scenario, + run: run + ) + ) + case .profiling: + // Collect traces during profiling run + Benchmarks.enableTracer( + with: Benchmarks.Configuration( + info: applicationInfo, + scenario: scenario, + run: run + ) + ) + + DatadogInternal.profiler = Profiler() + case .none: + break + } + + if run != .baseline { + // instrument the application with Datadog SDK + // when not in baseline run + scenario.instrument(with: applicationInfo) + } + + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = scenario.initialViewController + window?.makeKeyAndVisible() + + return true + } +} + +extension Benchmarks.Configuration { + init( + info: AppInfo, + scenario: SyntheticScenario, + run: SyntheticRun, + bundle: Bundle = .main, + sysctl: SysctlProviding = Sysctl(), + device: UIDevice = .current + ) { + self.init( + clientToken: info.clientToken, + apiKey: info.apiKey, + context: Benchmarks.Configuration.Context( + applicationIdentifier: bundle.bundleIdentifier!, + applicationName: bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as! String, + applicationVersion: bundle.object(forInfoDictionaryKey: "CFBundleVersion") as! String, + sdkVersion: "", + deviceModel: try! sysctl.model(), + osName: device.systemName, + osVersion: device.systemVersion, + run: run.rawValue, + scenario: scenario.name.rawValue, + branch: "" + ) + ) + } +} diff --git a/BenchmarkTests/Runner/BenchmarkProfiler.swift b/BenchmarkTests/Runner/BenchmarkProfiler.swift new file mode 100644 index 0000000000..8030f5b506 --- /dev/null +++ b/BenchmarkTests/Runner/BenchmarkProfiler.swift @@ -0,0 +1,26 @@ +/* + * 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 DatadogInternal +import DatadogBenchmarks + +internal final class Profiler: DatadogInternal.BenchmarkProfiler { + func tracer(operation: @autoclosure () -> String) -> any DatadogInternal.BenchmarkTracer { + DummyTracer() + } +} + +internal final class DummyTracer: DatadogInternal.BenchmarkTracer { + func startSpan(named: @autoclosure () -> String) -> any DatadogInternal.BenchmarkSpan { + DummySpan() + } +} + +internal final class DummySpan: DatadogInternal.BenchmarkSpan { + func stop() { } +} diff --git a/BenchmarkTests/Runner/Info.plist b/BenchmarkTests/Runner/Info.plist new file mode 100644 index 0000000000..1c5a6ca83d --- /dev/null +++ b/BenchmarkTests/Runner/Info.plist @@ -0,0 +1,19 @@ + + + + + DatadogConfiguration + + ApiKey + $(API_KEY) + ApplicationID + $(RUM_APPLICATION_ID) + ClientToken + $(CLIENT_TOKEN) + Environment + $(DD_ENV) + Site + $(DD_SITE) + + + diff --git a/BenchmarkTests/Runner/Scenarios/Scenario.swift b/BenchmarkTests/Runner/Scenarios/Scenario.swift new file mode 100644 index 0000000000..b844e8ce07 --- /dev/null +++ b/BenchmarkTests/Runner/Scenarios/Scenario.swift @@ -0,0 +1,24 @@ +/* + * 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 UIKit + +/// A `Scenario` is the entry-point of the Benchmark Runner Application. +/// +/// The compliant objects are responsible for initializing the SDK, enabling +/// Features, and create the initial view-controller. +protocol Scenario { + /// The initial view-controller of the scenario + var initialViewController: UIViewController { get } + + /// Start instrumenting the application by enabling the Datadog SDK and + /// its Features. + /// + /// - Parameter info: The application information to use during SDK + /// initialisation. + func instrument(with info: AppInfo) +} diff --git a/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift b/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.swift new file mode 100644 index 0000000000..65d116c8ff --- /dev/null +++ b/BenchmarkTests/Runner/Scenarios/SessionReplay/SessionReplayScenario.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 Foundation +import UIKit + +import DatadogCore +import DatadogRUM +import DatadogSessionReplay + +import UIKitCatalog + +struct SessionReplayScenario: Scenario { + var initialViewController: UIViewController { + let storyboard = UIStoryboard(name: "Main", bundle: UIKitCatalog.bundle) + return storyboard.instantiateInitialViewController()! + } + + func instrument(with info: AppInfo) { + Datadog.initialize( + with: .benchmark(info: info), + trackingConsent: .granted + ) + + RUM.enable( + with: RUM.Configuration( + applicationID: info.applicationID, + uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(), + uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate() + ) + ) + + SessionReplay.enable( + with: SessionReplay.Configuration( + replaySampleRate: 100, + defaultPrivacyLevel: .allow + ) + ) + + RUMMonitor.shared().addAttribute(forKey: "scenario", value: "SessionReplay") + } +} diff --git a/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift b/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift new file mode 100644 index 0000000000..4ec483d60e --- /dev/null +++ b/BenchmarkTests/Runner/Scenarios/SyntheticScenario.swift @@ -0,0 +1,80 @@ +/* + * 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 UIKit + +/// The Synthetics Scenario reads the `BENCHMARK_SCENARIO` environment +/// variable to instantiate a `Scenario` compliant object. +internal struct SyntheticScenario: Scenario { + /// The Synthetics benchmark scenario value. + internal enum Name: String { + case sessionReplay + } + /// The scenario's name. + let name: Name + + /// The underlying scenario. + private let _scenario: Scenario + + /// Creates the scenario by reading the `BENCHMARK_SCENARIO` value from the + /// environment variables. + /// + /// - Parameter processInfo: The `ProcessInfo` with environment variables + /// configured + init?(processInfo: ProcessInfo = .processInfo) { + guard + let rawValue = processInfo.environment["BENCHMARK_SCENARIO"], + let name = Name(rawValue: rawValue) + else { + return nil + } + + switch name { + case .sessionReplay: + _scenario = SessionReplayScenario() + } + + self.name = name + } + + var initialViewController: UIViewController { + _scenario.initialViewController + } + + func instrument(with info: AppInfo) { + _scenario.instrument(with: info) + } +} + +/// The Synthetics benchmark run. +/// +/// The run specifies the execution context of a benchmark scenrio. +/// Each execution will collect different type of benchmarking data: +/// - The `baseline` run collects various metrics during the scenario execution **without** +/// the Datadog SDK being initialised. +/// - The `instrumented` run collects the same metrics as `baseline` but **with** the +/// Datadog SDK initialised. Comparing the `baseline` and `instrumented` runs will provide +/// the overhead of the SDK for each metric. +/// - The `profiling` run will only collect traces of the SDK internal processes. +internal enum SyntheticRun: String { + case baseline + case instrumented + case profiling + case none + + /// Creates the scenario by reading the `BENCHMARK_RUN` value from the + /// environment variables. + /// + /// - Parameter processInfo: The `ProcessInfo` with environment variables + /// configured + init(processInfo: ProcessInfo = .processInfo) { + self = processInfo + .environment["BENCHMARK_RUN"] + .flatMap(Self.init(rawValue:)) + ?? .none + } +} diff --git a/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift b/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift new file mode 100755 index 0000000000..dfa876cbc1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ActivityIndicatorViewController.swift @@ -0,0 +1,81 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIActivityIndicatorView`. +*/ + +import UIKit + +class ActivityIndicatorViewController: BaseTableViewController { + + // Cell identifier for each activity indicator table view cell. + enum ActivityIndicatorKind: String, CaseIterable { + case mediumIndicator + case largeIndicator + case mediumTintedIndicator + case largeTintedIndicator + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.mediumIndicator.rawValue, + configHandler: configureMediumActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.largeIndicator.rawValue, + configHandler: configureLargeActivityIndicatorView) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted activity indicators available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MediumTintedIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.mediumTintedIndicator.rawValue, + configHandler: configureMediumTintedActivityIndicatorView), + CaseElement(title: NSLocalizedString("LargeTintedIndicatorTitle", bundle: .module, comment: ""), + cellID: ActivityIndicatorKind.largeTintedIndicator.rawValue, + configHandler: configureLargeTintedActivityIndicatorView) + ]) + } + } + + // MARK: - Configuration + + func configureMediumActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureMediumTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.medium + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + + func configureLargeTintedActivityIndicatorView(_ activityIndicator: UIActivityIndicatorView) { + activityIndicator.style = UIActivityIndicatorView.Style.large + activityIndicator.hidesWhenStopped = true + activityIndicator.color = UIColor.systemPurple + + activityIndicator.startAnimating() + // When the activity is done, be sure to use UIActivityIndicatorView.stopAnimating(). + } + +} diff --git a/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift b/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift new file mode 100755 index 0000000000..40ae167374 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/AlertControllerViewController.swift @@ -0,0 +1,317 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +The view controller that demonstrates how to use `UIAlertController`. +*/ + +import UIKit + +class AlertControllerViewController: UITableViewController { + // MARK: - Properties + + weak var secureTextAlertAction: UIAlertAction? + + private enum StyleSections: Int { + case alertStyleSection = 0 + case actionStyleSection + } + + private enum AlertStyleTest: Int { + // Alert style alerts. + case showSimpleAlert = 0 + case showOkayCancelAlert + case showOtherAlert + case showTextEntryAlert + case showSecureTextEntryAlert + } + + private enum ActionSheetStyleTest: Int { + // Action sheet style alerts. + case showOkayCancelActionSheet = 0 + case howOtherActionSheet + } + + private var textDidChangeObserver: Any? = nil + + // MARK: - UIAlertControllerStyleAlert Style Alerts + + /// Show an alert with an "OK" button. + func showSimpleAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the action. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The simple alert's cancel action occurred.") + } + + // Add the action. + alertController.addAction(cancelAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show an alert with an "OK" and "Cancel" button. + func showOkayCancelAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertCotroller = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert's other action occurred.") + } + + // Add the actions. + alertCotroller.addAction(cancelAction) + alertCotroller.addAction(otherAction) + + present(alertCotroller, animated: true, completion: nil) + } + + /// Show an alert with two custom buttons. + func showOtherAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitleOne = NSLocalizedString("Choice One", bundle: .module, comment: "") + let otherButtonTitleTwo = NSLocalizedString("Choice Two", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Other\" alert's cancel action occurred.") + } + + let otherButtonOneAction = UIAlertAction(title: otherButtonTitleOne, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button one action occurred.") + } + + let otherButtonTwoAction = UIAlertAction(title: otherButtonTitleTwo, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert's other button two action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherButtonOneAction) + alertController.addAction(otherButtonTwoAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a text entry alert with two custom buttons. + func showTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for text entry. + alertController.addTextField { _ in + // If you need to customize the text field, you can do so here. + } + + // Create the actions. + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Text Entry\" alert's cancel action occurred.") + } + + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Text Entry\" alert's other action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + /// Show a secure text entry alert with two custom buttons. + func showSecureTextEntryAlert() { + let title = NSLocalizedString("A Short Title is Best", bundle: .module, comment: "") + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("OK", bundle: .module, comment: "") + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + // Add the text field for the secure text entry. + alertController.addTextField { textField in + if let observer = self.textDidChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + /** Listen for changes to the text field's text so that we can toggle the current + action's enabled property based on whether the user has entered a sufficiently + secure entry. + */ + self.textDidChangeObserver = + NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, + object: textField, + queue: OperationQueue.main, + using: { (notification) in + if let textField = notification.object as? UITextField { + // Enforce a minimum length of >= 5 characters for secure text alerts. + if let alertAction = self.secureTextAlertAction { + if let text = textField.text { + alertAction.isEnabled = text.count >= 5 + } else { + alertAction.isEnabled = false + } + } + } + }) + + textField.isSecureTextEntry = true + } + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's cancel action occurred.") + } + + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Secure Text Entry\" alert's other action occurred.") + } + + /** The text field initially has no text in the text field, so we'll disable it for now. + It will be re-enabled when the first character is typed. + */ + otherAction.isEnabled = false + + /** Hold onto the secure text alert action to toggle the enabled / disabled + state when the text changed. + */ + secureTextAlertAction = otherAction + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(otherAction) + + present(alertController, animated: true, completion: nil) + } + + // MARK: - UIAlertControllerStyleActionSheet Style Alerts + + // Show a dialog with an "OK" and "Cancel" button. + func showOkayCancelActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let cancelButtonTitle = NSLocalizedString("Cancel", bundle: .module, comment: "") + let destructiveButtonTitle = NSLocalizedString("Confirm", bundle: .module, comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in + Swift.debugPrint("The \"OK/Cancel\" alert action sheet's cancel action occurred.") + } + + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Confirm\" alert action sheet's destructive action occurred.") + } + + // Add the actions. + alertController.addAction(cancelAction) + alertController.addAction(destructiveAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } + + // Show a dialog with two custom buttons. + func showOtherActionSheet(_ selectedIndexPath: IndexPath) { + let message = NSLocalizedString("A message needs to be a short, complete sentence.", bundle: .module, comment: "") + let destructiveButtonTitle = NSLocalizedString("Destructive Choice", bundle: .module, comment: "") + let otherButtonTitle = NSLocalizedString("Safe Choice", bundle: .module, comment: "") + + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + + // Create the actions. + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .destructive) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's destructive action occurred.") + } + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in + Swift.debugPrint("The \"Other\" alert action sheet's other action occurred.") + } + + // Add the actions. + alertController.addAction(destructiveAction) + alertController.addAction(otherAction) + + // Configure the alert controller's popover presentation controller if it has one. + if let popoverPresentationController = alertController.popoverPresentationController { + // Note for popovers the Cancel button is hidden automatically. + + // This method expects a valid cell to display from. + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! + popoverPresentationController.sourceRect = selectedCell.frame + popoverPresentationController.sourceView = view + popoverPresentationController.permittedArrowDirections = .up + } + + present(alertController, animated: true, completion: nil) + } + +} + +// MARK: - UITableViewDelegate + +extension AlertControllerViewController { + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + switch indexPath.section { + case StyleSections.alertStyleSection.rawValue: + // Alert style. + switch indexPath.row { + case AlertStyleTest.showSimpleAlert.rawValue: + showSimpleAlert() + case AlertStyleTest.showOkayCancelAlert.rawValue: + showOkayCancelAlert() + case AlertStyleTest.showOtherAlert.rawValue: + showOtherAlert() + case AlertStyleTest.showTextEntryAlert.rawValue: + showTextEntryAlert() + case AlertStyleTest.showSecureTextEntryAlert.rawValue: + showSecureTextEntryAlert() + default: break + } + case StyleSections.actionStyleSection.rawValue: + switch indexPath.row { + // Action sheet style. + case ActionSheetStyleTest.showOkayCancelActionSheet.rawValue: + showOkayCancelActionSheet(indexPath) + case ActionSheetStyleTest.howOtherActionSheet.rawValue: + showOtherActionSheet(indexPath) + default: break + } + default: break + } + + tableView.deselectRow(at: indexPath, animated: true) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000000..d8db8d65fd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json new file mode 100755 index 0000000000..73c00596a7 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json new file mode 100755 index 0000000000..4e892e1870 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png new file mode 100755 index 0000000000..b4b3b382c4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_1.imageset/Flowers_1.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json new file mode 100755 index 0000000000..f58b0f113b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Flowers_2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png new file mode 100755 index 0000000000..149520fb4d Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/Flowers_2.imageset/Flowers_2.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json new file mode 100755 index 0000000000..5e6240639e --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png new file mode 100755 index 0000000000..c65e3961d8 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png new file mode 100755 index 0000000000..6e68c5bd05 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png new file mode 100755 index 0000000000..be149037da Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background.imageset/stepper_and_segment_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json new file mode 100755 index 0000000000..fdb1b66722 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_disabled_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png new file mode 100755 index 0000000000..7abdc2bcb4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png new file mode 100755 index 0000000000..0580445308 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png new file mode 100755 index 0000000000..29805f326e Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_disabled.imageset/stepper_and_segment_background_disabled_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json new file mode 100755 index 0000000000..bca57e87be --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stepper_and_segment_background_highlighted_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png new file mode 100755 index 0000000000..c623650ddc Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png new file mode 100755 index 0000000000..2a9ee5c1c4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png new file mode 100755 index 0000000000..cf0a17a548 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/background_highlighted.imageset/stepper_and_segment_background_highlighted_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json new file mode 100755 index 0000000000..68464e93ab --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "search_bar_bg_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_bg_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "search_bar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png new file mode 100755 index 0000000000..486f5413bb Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png new file mode 100755 index 0000000000..d20a0bb6e7 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png new file mode 100755 index 0000000000..88ecb2f12d Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/search_bar_background.imageset/search_bar_bg_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json new file mode 100755 index 0000000000..ea6fe64740 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_blue_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_blue_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png new file mode 100755 index 0000000000..3f10475947 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png new file mode 100755 index 0000000000..7ba3616579 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png new file mode 100755 index 0000000000..7f47c6e305 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_blue_track.imageset/slider_blue_track_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json new file mode 100755 index 0000000000..bad86401df --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "slider_green_track_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "slider_green_track_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png new file mode 100755 index 0000000000..dd6087d24a Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png new file mode 100755 index 0000000000..5c6cd69e86 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png new file mode 100755 index 0000000000..75a6915a89 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/slider_green_track.imageset/slider_green_track_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json new file mode 100644 index 0000000000..86976ae85a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "stepper_and_segment_segment_divider_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stepper_and_segment_segment_divider_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "stepper_and_segment_divider_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png new file mode 100644 index 0000000000..1aabd6a584 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_divider_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png new file mode 100644 index 0000000000..2d092bd7a4 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png new file mode 100644 index 0000000000..168bdfd472 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/stepper_and_segment_divider.imageset/stepper_and_segment_segment_divider_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json new file mode 100755 index 0000000000..7162851034 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/Contents.json @@ -0,0 +1,45 @@ +{ + "images" : [ + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_1x.png", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "3-part-horizontal", + "center" : { + "mode" : "stretch", + "width" : 0 + }, + "cap-insets" : { + "right" : 1, + "left" : 1 + } + }, + "idiom" : "universal", + "filename" : "text_field_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png new file mode 100755 index 0000000000..5c3c3cf6a5 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png new file mode 100755 index 0000000000..abf9f0a012 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png new file mode 100755 index 0000000000..b121f9db65 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_background.imageset/text_field_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json new file mode 100755 index 0000000000..64a5b15a81 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "text_field_purple_right_view_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png new file mode 100755 index 0000000000..c450af9689 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png new file mode 100755 index 0000000000..e81719e878 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png new file mode 100755 index 0000000000..2957cbb6d3 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_field_purple_right_view.imageset/text_field_purple_right_view_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json new file mode 100755 index 0000000000..fb8876d7a1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Sunset_5.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png new file mode 100755 index 0000000000..3ce67dff32 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_attachment.imageset/Sunset_5.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json new file mode 100755 index 0000000000..e36b88e424 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/text_view_background.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "0.000", + "blue" : "0.000", + "green" : "0.000" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json new file mode 100755 index 0000000000..479569c484 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_segmented_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json new file mode 100755 index 0000000000..479569c484 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/tinted_stepper_control.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.209", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.938" + } + } + }, + { + "idiom" : "ipad", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.031", + "alpha" : "1.000", + "blue" : "0.500", + "green" : "0.702" + } + } + } + ] +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json new file mode 100755 index 0000000000..1756a035cc --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "toolbar_background_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "toolbar_background_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png new file mode 100755 index 0000000000..f37907ff93 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_1x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png new file mode 100755 index 0000000000..a271d28de7 Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_2x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png new file mode 100755 index 0000000000..486f5413bb Binary files /dev/null and b/BenchmarkTests/UIKitCatalog/Assets.xcassets/toolbar_background.imageset/toolbar_background_3x.png differ diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard new file mode 100755 index 0000000000..40c0d74348 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ActivityIndicatorViewController.storyboard @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard new file mode 100755 index 0000000000..e52293c5a6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/AlertControllerViewController.storyboard @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard new file mode 100755 index 0000000000..0076e70c5c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ButtonViewController.storyboard @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard new file mode 100755 index 0000000000..55e9ee6d73 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ColorPickerViewController.storyboard @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf b/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf new file mode 100755 index 0000000000..c9f3ebb74f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Credits.rtf @@ -0,0 +1,10 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2617 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} +{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} +\vieww9000\viewh8400\viewkind0 +\pard\tx960\tx1920\tx2880\tx3840\tx4800\tx5760\tx6720\tx7680\tx8640\tx9600\qc\partightenfactor0 + +\f0\fs20 \cf2 Demonstrates how to use {\field{\*\fldinst{HYPERLINK "https://developer.apple.com/documentation/uikit"}}{\fldrslt UIKit}}\ +views, controls and pickers.\ +} \ No newline at end of file diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard new file mode 100755 index 0000000000..6b60d8eb4b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard new file mode 100755 index 0000000000..bd83634c34 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomSearchBarViewController.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard new file mode 100755 index 0000000000..80366b55e4 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/CustomToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard new file mode 100755 index 0000000000..2d752ae3c4 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DatePickerController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard new file mode 100755 index 0000000000..aac4dffef5 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultPageControlViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard new file mode 100755 index 0000000000..3053676020 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultSearchBarViewController.storyboard @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + Title + Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard new file mode 100755 index 0000000000..248ff5042a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/DefaultToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard new file mode 100755 index 0000000000..b28c89f0a7 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/FontPickerViewController.storyboard @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard new file mode 100755 index 0000000000..c0c74769df --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ImagePickerViewController.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard new file mode 100755 index 0000000000..886c071308 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ImageViewController.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings b/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings new file mode 100755 index 0000000000..78c04666cf --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Localizable.strings @@ -0,0 +1,173 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Strings used across the application via the NSLocalizedString API. +*/ + +"OK" = "OK"; +"Cancel" = "Cancel"; +"Confirm" = "Confirm"; +"Destructive Choice" = "Destructive Choice"; +"Safe Choice" = "Safe Choice"; +"A Short Title Is Best" = "A Short Title Is Best"; +"A message needs to be a short, complete sentence." = "A message needs to be a short, complete sentence."; +"Choice One" = "Choice One"; +"Choice Two" = "Choice Two"; +"Button" = "Button"; +"Pressed" = "Pressed"; +"X Button" = "X Button"; +"Image" = "Image"; +"bold" = "bold"; +"highlighted" = "highlighted"; +"underlined" = "underlined"; +"tinted" = "tinted"; +"Placeholder text" = "Placeholder text"; +"Enter search text" = "Enter search text"; +"Red color component value" = "Red color component value"; +"Green color component value" = "Green color component value"; +"Blue color component value" = "Blue color component value"; +"Animated" = "A slide show of images"; + +"Airplane" = "Airplane"; +"Gift" = "Gift"; +"Burst" = "Burst"; + +"An error occurred:" = "An error occurred:"; + +"ButtonsTitle" = "Buttons"; +"MenuButtonsTitle" = "Menu Buttons"; +"PointerInteractionButtonsTitle" = "Pointer Interaction"; +"PageControlTitle" = "Page Controls"; +"SearchBarsTitle" = "Search Bars"; +"SegmentedControlsTitle" = "Segmented Controls"; +"SlidersTitle" = "Sliders"; +"SteppersTitle" = "Steppers"; +"SwitchesTitle" = "Switches"; +"TextFieldsTitle" = "Text Fields"; + +"ActivityIndicatorsTitle" = "Activity Indicators"; +"AlertControllersTitle" = "Alert Controllers"; + +"ImagesTitle" = "Image Views"; +"ImageViewTitle" = "Image View"; +"SymbolsTitle" = "SF Symbol"; + +"ProgressViewsTitle" = "Progress Views"; +"StackViewsTitle" = "Stack Views"; +"TextViewTitle" = "Text View"; +"ToolbarsTitle" = "Toolbars"; +"VisualEffectTitle" = "Visual Effect"; +"WebViewTitle" = "Web View"; + +"DatePickerTitle" = "Date Picker"; +"PickerViewTitle" = "Picker View"; +"ColorPickerTitle" = "Color Picker"; +"FontPickerTitle" = "Font Picker"; +"ImagePickerTitle" = "Image Picker"; + +"DefaultSearchBarTitle" = "Default Search Bar"; +"CustomSearchBarTitle" = "Custom Search Bar"; + +"DefaultToolBarTitle" = "Default Toolbar"; +"TintedToolbarTitle" = "Tinted Toolbar"; +"CustomToolbarBarTitle" = "Custom Toolbar"; + +"ChooseItemTitle" = "Choose an item:"; +"ItemTitle" = "Item %@"; + +"SampleFontTitle" = "Sample Font"; + +"CheckTitle" = "Check"; +"SearchTitle" = "Search"; +"ToolsTitle" = "Tools"; + +"DefaultPageControlTitle" = "Page Control"; +"CustomPageControlTitle" = "Custom Page Control"; + +"SwitchTitle" = "Title"; + +"DefaultSwitchTitle" = "Default"; +"CheckboxSwitchTitle" = "Checkbox"; +"TintedSwitchTitle" = "Tinted"; + +"ImageToolTipTitle" = "This is a list of flower photos obtained from the sample's asset library."; +"GrayStyleButtonToolTipTitle" = "This is a gray-style system button."; +"TintedStyleButtonToolTipTitle" = "This is a tinted-style system button."; +"FilledStyleButtonToolTipTitle" = "This is a filled-style system button."; +"CapsuleStyleButtonToolTipTitle" = "This is a capsule-style system button."; +"CartFilledButtonToolTipTitle" = "Button cart is filled"; +"CartEmptyButtonToolTipTitle" = "Button cart is empty"; +"XButtonToolTipTitle" = "X Button"; +"PersonButtonToolTipTitle" = "Person Button"; +"VisualEffectToolTipTitle" = "This demonstrates how to use a UIVisualEffectView on top of an UIImageView and underneath a UITextView."; + +"VisualEffectTextContent" = "This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView. This is a UITextView with text content placed inside a UIVisualEffectView."; + +"DefaultTitle" = "Default"; +"DetailDisclosureTitle" = "Detail Disclosure"; +"AddContactTitle" = "Add Contact"; +"CloseTitle" = "Close"; +"GrayTitle" = "Gray"; +"TintedTitle" = "Tinted"; +"FilledTitle" = "Filled"; +"CornerStyleTitle" = "Corner Style"; +"ToggleTitle" = "Toggle"; +"ButtonColorTitle" = "Colored Title"; + +"ImageTitle" = "Image"; +"AttributedStringTitle" = "Attributed String"; +"SymbolTitle" = "Symbol"; + +"LargeSymbolTitle" = "Large Symbol"; +"SymbolStringTitle" = "Symbol + String"; +"StringSymbolTitle" = "String + Symbol"; +"MultiTitleTitle" = "Multi-Title"; +"BackgroundTitle" = "Background"; + +"UpdateActivityHandlerTitle" = "Update Activity Handler"; +"UpdateHandlerTitle" = "Update Handler"; +"UpdateImageHandlerTitle" = "Update Handler (Button Image)"; + +"AddToCartTitle" = "Add to Cart"; + +"DropDownTitle" = "Drop Down"; +"DropDownProgTitle" = "Drop Down Programmatic"; +"DropDownMultiActionTitle" = "Drop Down Multi-Action"; +"DropDownButtonSubMenuTitle" = "Drop Down Submenu"; +"PopupSelection" = "Popup Selection"; +"PopupMenuTitle" = "Popup Menu"; + +"CustomSegmentsTitle" = "Custom Segments"; +"CustomBackgroundTitle" = "Custom Background"; +"ActionBasedTitle" = "Action Based"; + +"CustomTitle" = "Custom"; +"MinMaxImagesTitle" = "Min and Max Images"; + +"DefaultStepperTitle" = "Default Stepper"; +"TintedStepperTitle" = "Tinted Stepper"; +"CustomStepperTitle" = "Custom Stepper"; + +"PlainSymbolTitle" = "Default"; +"TintedSymbolTitle" = "Tinted"; +"LargeSymbolTitle" = "Large"; +"HierarchicalSymbolTitle" = "Hierarchical Color"; +"PaletteSymbolTitle" = "Palette Color"; +"PreferringMultiColorSymbolTitle" = "Preferring Multi-Color"; + +"DefaultTextFieldTitle" = "Default"; +"TintedTextFieldTitle" = "Tinted"; +"SecuretTextFieldTitle" = "Secure"; +"SpecificKeyboardTextFieldTitle" = "Specific Keyboard"; +"CustomTextFieldTitle" = "Custom"; +"SearchTextFieldTitle" = "Search"; + +"MediumIndicatorTitle" = "Medium"; +"LargeIndicatorTitle" = "Large"; +"MediumTintedIndicatorTitle" = "Medium Tinted"; +"LargeTintedIndicatorTitle" = "Large Tinted"; + +"ProgressDefaultTitle" = "Default"; +"ProgressBarTitle" = "Bar"; +"ProgressTintedTitle" = "Tinted"; diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard new file mode 100755 index 0000000000..afda7f7f21 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/Main.storyboard @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard new file mode 100755 index 0000000000..6e7ecf37c8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/MenuButtonViewController.storyboard @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard new file mode 100755 index 0000000000..f5209519a6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/PickerViewController.storyboard @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard new file mode 100755 index 0000000000..664719dc74 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/PointerInteractionButtonViewController.storyboard @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard new file mode 100755 index 0000000000..efc642095c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/ProgressViewController.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard new file mode 100755 index 0000000000..4166c5b05e --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SegmentedControlViewController.storyboard @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard new file mode 100755 index 0000000000..4420a0f00a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SliderViewController.storyboard @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard new file mode 100755 index 0000000000..a3b3d88723 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/StackViewController.storyboard @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard new file mode 100755 index 0000000000..a28bc8f7d3 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/StepperViewController.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard new file mode 100755 index 0000000000..69655b053b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SwitchViewController.storyboard @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard new file mode 100755 index 0000000000..cecdae8104 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/SymbolViewController.storyboard @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard new file mode 100755 index 0000000000..3e2676b938 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TextFieldViewController.storyboard @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard new file mode 100755 index 0000000000..1161aae7b8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TextViewController.storyboard @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + This is a UITextView that uses attributed text. You can programmatically modify the display of the text by making it bold, highlighted, underlined, tinted, symbols, and more. These attributes are defined in NSAttributedString.h. You can even embed attachments in an NSAttributedString! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard new file mode 100755 index 0000000000..b5b460b356 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/TintedToolbarViewController.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard new file mode 100755 index 0000000000..12d43a517f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/VisualEffectViewController.storyboard @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard b/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard new file mode 100755 index 0000000000..d335aaaa16 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/WebViewController.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BenchmarkTests/UIKitCatalog/Base.lproj/content.html b/BenchmarkTests/UIKitCatalog/Base.lproj/content.html new file mode 100755 index 0000000000..c2dc89958f --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/Base.lproj/content.html @@ -0,0 +1,16 @@ + + + + WKWebView + + + +
+

This is HTML content inside a WKWebView.

+ For more information refer to developer.apple.com + + diff --git a/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift b/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift new file mode 100644 index 0000000000..9320cdd193 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/BaseTableViewController.swift @@ -0,0 +1,52 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A base class used for all UITableViewControllers in this sample app. +*/ + +import UIKit + +class BaseTableViewController: UITableViewController { + // List of table view cell test cases. + var testCells = [CaseElement]() + + func centeredHeaderView(_ title: String) -> UITableViewHeaderFooterView { + // Set the header title and make it centered. + let headerView: UITableViewHeaderFooterView = UITableViewHeaderFooterView() + var content = UIListContentConfiguration.groupedHeader() + content.text = title + content.textProperties.alignment = .center + headerView.contentConfiguration = content + return headerView + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return centeredHeaderView(testCells[section].title) + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return testCells[section].title + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return testCells.count + } + + override func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let view = cellTest.targetView(cell) { + cellTest.configHandler(view) + } + return cell + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift b/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift new file mode 100755 index 0000000000..2de5fb0d6c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ButtonViewController+Configs.swift @@ -0,0 +1,470 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Configuration functions for all the UIButtons found in ButtonViewController. +*/ + +import UIKit + +extension ButtonViewController: UIToolTipInteractionDelegate { + + func configureSystemTextButton(_ button: UIButton) { + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: []) + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + func configureSystemDetailDisclosureButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + func configureSystemContactAddButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureCloseButton(_ button: UIButton) { + // Nothing particular to set here, it's all been done in the storyboard. + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleGrayButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + let config = UIButton.Configuration.gray() + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("GrayStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleTintedButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + + var config = UIButton.Configuration.tinted() + + /** To keep the look the same betwen iOS and macOS: + For tinted color to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + // The following will make the button title red and background a lighter red. + config.baseBackgroundColor = .systemRed + config.baseForegroundColor = .systemRed + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("TintedStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.configuration = config + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureStyleFilledButton(_ button: UIButton) { + // Note this can be also be done in the storyboard for this button. + var config = UIButton.Configuration.filled() + config.background.backgroundColor = .systemRed + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("FilledStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureCornerStyleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For cornerStyle to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + var config = UIButton.Configuration.gray() + config.cornerStyle = .capsule + button.configuration = config + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.toolTip = NSLocalizedString("CapsuleStyleButtonToolTipTitle", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureImageButton(_ button: UIButton) { + // To create this button in code you can use `UIButton.init(type: .system)`. + + // Set the tint color to the button's image. + if let image = UIImage(systemName: "xmark") { + let imageButtonNormalImage = image.withTintColor(.systemPurple) + button.setImage(imageButtonNormalImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("X", bundle: .module, comment: "") + + if #available(iOS 15, *) { + button.toolTip = NSLocalizedString("XButtonToolTipTitle", bundle: .module, comment: "") + } + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureAttributedTextSystemButton(_ button: UIButton) { + let buttonTitle = NSLocalizedString("Button", bundle: .module, comment: "") + + // Set the button's title for normal state. + let normalTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue + ] + + let normalAttributedTitle = NSAttributedString(string: buttonTitle, attributes: normalTitleAttributes) + button.setAttributedTitle(normalAttributedTitle, for: .normal) + + // Set the button's title for highlighted state (note this is not supported in Mac Catalyst). + let highlightedTitleAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.thick.rawValue + ] + let highlightedAttributedTitle = NSAttributedString(string: buttonTitle, attributes: highlightedTitleAttributes) + button.setAttributedTitle(highlightedAttributedTitle, for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = buttonImage + button.configuration = buttonConfig + + button.toolTip = NSLocalizedString("PersonButtonToolTipTitle", bundle: .module, comment: "") + } else { + button.setImage(buttonImage, for: .normal) + } + + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .large) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureLargeSymbolButton(_ button: UIButton) { + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // For iOS 15 use the UIButtonConfiguration to change the size. + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + buttonConfig.image = buttonImage + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + } + + // Since this button title is just an image, add an accessibility label. + button.accessibilityLabel = NSLocalizedString("Person", bundle: .module, comment: "") + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureSymbolTextButton(_ button: UIButton) { + // Button with image to the left of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + buttonConfig.image = buttonImage + + button.configuration = buttonConfig + } else { + button.setImage(buttonImage, for: .normal) + + // Set up the symbol image size to match that of the title font size. + let config = UIImage.SymbolConfiguration(textStyle: .body, scale: .small) + button.setPreferredSymbolConfiguration(config, forImageIn: .normal) + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureTextSymbolButton(_ button: UIButton) { + // Button with image to the right of the title. + + let buttonImage = UIImage(systemName: "person") + + if #available(iOS 15, *) { + // Use UIButtonConfiguration to set the image. + var buttonConfig = UIButton.Configuration.plain() + + // Set up the symbol image size to match that of the title font size. + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + + buttonConfig.image = buttonImage + + // Set the image placement to the right of the title. + /** For image placement to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + buttonConfig.imagePlacement = .trailing + + button.configuration = buttonConfig + } + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Person", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureMultiTitleButton(_ button: UIButton) { + /** To keep the look the same betwen iOS and macOS: + For setTitle(.highlighted) to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: .normal) + button.setTitle(NSLocalizedString("Pressed", bundle: .module, comment: ""), for: .highlighted) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureToggleButton(button: UIButton) { + button.changesSelectionAsPrimaryAction = true // This makes the button style a "toggle button". + } + + func configureTitleTextButton(_ button: UIButton) { + // Note: Only for iOS the title's color can be changed. + button.setTitleColor(UIColor.systemGreen, for: [.normal]) + button.setTitleColor(UIColor.systemRed, for: [.highlighted]) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + func configureBackgroundButton(_ button: UIButton) { + if #available(iOS 15, *) { + /** To keep the look the same betwen iOS and macOS: + For setBackgroundImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + } + + button.setBackgroundImage(UIImage(named: "background", in: .module, compatibleWith: nil), for: .normal) + button.setBackgroundImage(UIImage(named: "background_highlighted", in: .module, compatibleWith: nil), for: .highlighted) + button.setBackgroundImage(UIImage(named: "background_disabled", in: .module, compatibleWith: nil), for: .disabled) + + button.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) + } + + // This handler is called when this button needs updating. + @available(iOS 15.0, *) + func configureUpdateActivityHandlerButton(_ button: UIButton) { + let activityUpdateHandler: (UIButton) -> Void = { button in + /// Shows an activity indicator in place of an image. Its placement is controlled by the `imagePlacement` property. + + // Start with the current button's configuration. + var config = button.configuration + config?.showsActivityIndicator = button.isSelected ? false : true + button.configuration = config + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "tray") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .body) + button.configuration = buttonConfig + + // Set the button's title and font. + button.setTitle(NSLocalizedString("Button", bundle: .module, comment: ""), for: []) + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = activityUpdateHandler + + // For this button to include an activity indicator next to the title, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.baseBackgroundColor = button.isSelected + ? UIColor.systemPink.withAlphaComponent(0.4) + : UIColor.systemPink + } + + let buttonConfig = UIButton.Configuration.filled() + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use baseBackgroundColor for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + @available(iOS 15.0, *) + func configureUpdateImageHandlerButton(_ button: UIButton) { + // This is called when a button needs an update. + let colorUpdateHandler: (UIButton) -> Void = { button in + button.configuration?.image = + button.isSelected ? UIImage(systemName: "cart.fill") : UIImage(systemName: "cart") + button.toolTip = + button.isSelected ? + NSLocalizedString("CartFilledButtonToolTipTitle", bundle: .module, comment: "") : + NSLocalizedString("CartEmptyButtonToolTipTitle", bundle: .module, comment: "") + } + + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.image = UIImage(systemName: "cart") + buttonConfig.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .largeTitle) + button.configuration = buttonConfig + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + button.configurationUpdateHandler = colorUpdateHandler + + // For this button to use the updateHandler to change it's icon for the visual toggle state, keep the iPad behavior. + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.setTitle("", for: []) // No title, just an image. + button.isSelected = false + + button.addTarget(self, action: #selector(ButtonViewController.toggleButtonClicked(_:)), for: .touchUpInside) + } + + // MARK: - Add To Cart Button + + @available(iOS 15.0, *) + func toolTipInteraction(_ interaction: UIToolTipInteraction, configurationAt point: CGPoint) -> UIToolTipConfiguration? { + let formatString = NSLocalizedString("Cart Tooltip String", + bundle: .module, + comment: "Cart Tooltip String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + return UIToolTipConfiguration(toolTip: resultString) + } + + @available(iOS 15.0, *) + func addToCart(action: UIAction) { + cartItemCount = cartItemCount > 0 ? 0 : 12 + if let button = action.sender as? UIButton { + button.setNeedsUpdateConfiguration() + } + } + + @available(iOS 15.0, *) + func configureAddToCartButton(_ button: UIButton) { + var config = UIButton.Configuration.filled() + config.buttonSize = .large + config.image = UIImage(systemName: "cart.fill") + config.title = "Add to Cart" + config.cornerStyle = .capsule + config.baseBackgroundColor = UIColor.systemTeal + button.configuration = config + + button.toolTip = "" // The value will be determined in its delegate. + button.toolTipInteraction?.delegate = self + + button.addAction(UIAction(handler: addToCart(action:)), for: .touchUpInside) + + // For this button to include subtitle and larger size, the behavioral style needs to be set to ".pad". + if traitCollection.userInterfaceIdiom == .mac { + button.preferredBehavioralStyle = .pad + } + + button.changesSelectionAsPrimaryAction = true // This turns on the toggle behavior. + + // This handler is called when this button needs updating. + button.configurationUpdateHandler = { + [unowned self] button in + + // Start with the current button's configuration. + var newConfig = button.configuration + + if button.isSelected { + // The button was clicked or tapped. + newConfig?.image = cartItemCount > 0 + ? UIImage(systemName: "cart.fill.badge.plus") + : UIImage(systemName: "cart.badge.plus") + + let formatString = NSLocalizedString("Cart Items String", + bundle: .module, + comment: "Cart Items String format to be found in Localizable.stringsdict") + let resultString = String.localizedStringWithFormat(formatString, cartItemCount) + newConfig?.subtitle = resultString + } else { + // As the button is highlighted (pressed), apply a temporary image and subtitle. + newConfig?.image = UIImage(systemName: "cart.fill") + newConfig?.subtitle = "" + } + + newConfig?.imagePadding = 8 // Add a litle more space between the icon and button title. + + // Note: To change the padding between the title and subtitle, set "titlePadding". + // Note: To change the padding around the perimeter of the button, set "contentInsets". + + button.configuration = newConfig + } + } + + // MARK: - Button Actions + + @objc + func buttonClicked(_ sender: UIButton) { + Swift.debugPrint("Button was clicked.") + } + + @objc + func toggleButtonClicked(_ sender: UIButton) { + Swift.debugPrint("Toggle action: \(sender)") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ButtonViewController.swift b/BenchmarkTests/UIKitCatalog/ButtonViewController.swift new file mode 100755 index 0000000000..0c9f0e1e48 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ButtonViewController.swift @@ -0,0 +1,156 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIButton`. + The buttons are created using storyboards, but each of the system buttons can be created in code by + using the UIButton.init(type buttonType: UIButtonType) initializer. + + See the UIButton interface for a comprehensive list of the various UIButtonType values. +*/ + +import UIKit + +class ButtonViewController: BaseTableViewController { + + // Cell identifier for each button table view cell. + enum ButtonKind: String, CaseIterable { + case buttonSystem + case buttonDetailDisclosure + case buttonSystemAddContact + case buttonClose + case buttonStyleGray + case buttonStyleTinted + case buttonStyleFilled + case buttonCornerStyle + case buttonToggle + case buttonTitleColor + case buttonImage + case buttonAttrText + case buttonSymbol + case buttonLargeSymbol + case buttonTextSymbol + case buttonSymbolText + case buttonMultiTitle + case buttonBackground + case addToCartButton + case buttonUpdateActivityHandler + case buttonUpdateHandler + case buttonImageUpdateHandler + } + + // MARK: - Properties + + // "Add to Cart" Button + var cartItemCount: Int = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSystem.rawValue, + configHandler: configureSystemTextButton), + CaseElement(title: NSLocalizedString("DetailDisclosureTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonDetailDisclosure.rawValue, + configHandler: configureSystemDetailDisclosureButton), + CaseElement(title: NSLocalizedString("AddContactTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSystemAddContact.rawValue, + configHandler: configureSystemContactAddButton), + CaseElement(title: NSLocalizedString("CloseTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonClose.rawValue, + configHandler: configureCloseButton) + ]) + + if #available(iOS 15, *) { + // These button styles are available on iOS 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("GrayTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleGray.rawValue, + configHandler: configureStyleGrayButton), + CaseElement(title: NSLocalizedString("TintedTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleTinted.rawValue, + configHandler: configureStyleTintedButton), + CaseElement(title: NSLocalizedString("FilledTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonStyleFilled.rawValue, + configHandler: configureStyleFilledButton), + CaseElement(title: NSLocalizedString("CornerStyleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonCornerStyle.rawValue, + configHandler: configureCornerStyleButton), + CaseElement(title: NSLocalizedString("ToggleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonToggle.rawValue, + configHandler: configureToggleButton) + ]) + } + + if traitCollection.userInterfaceIdiom != .mac { + // Colored button titles only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ButtonColorTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonTitleColor.rawValue, + configHandler: configureTitleTextButton) + ]) + } + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ImageTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonImage.rawValue, + configHandler: configureImageButton), + CaseElement(title: NSLocalizedString("AttributedStringTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonAttrText.rawValue, + configHandler: configureAttributedTextSystemButton), + CaseElement(title: NSLocalizedString("SymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSymbol.rawValue, + configHandler: configureSymbolButton) + ]) + + if #available(iOS 15, *) { + // This case uses UIButtonConfiguration which is available on iOS 15 or later. + if traitCollection.userInterfaceIdiom != .mac { + // UIButtonConfiguration for large images available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("LargeSymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonLargeSymbol.rawValue, + configHandler: configureLargeSymbolButton) + ]) + } + } + + if #available(iOS 15, *) { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("StringSymbolTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonTextSymbol.rawValue, + configHandler: configureTextSymbolButton), + CaseElement(title: NSLocalizedString("SymbolStringTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonSymbolText.rawValue, + configHandler: configureSymbolTextButton), + + CaseElement(title: NSLocalizedString("BackgroundTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonBackground.rawValue, + configHandler: configureBackgroundButton), + + // Multi-title button: title for normal and highlight state, setTitle(.highlighted) is for iOS 15 and later. + CaseElement(title: NSLocalizedString("MultiTitleTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonMultiTitle.rawValue, + configHandler: configureMultiTitleButton), + + // Various button effects done to the addToCartButton are available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("AddToCartTitle", bundle: .module, comment: ""), + cellID: ButtonKind.addToCartButton.rawValue, + configHandler: configureAddToCartButton), + + // UIButtonConfiguration with updateHandlers is available only on iOS 15 or later. + CaseElement(title: NSLocalizedString("UpdateActivityHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonUpdateActivityHandler.rawValue, + configHandler: configureUpdateActivityHandlerButton), + CaseElement(title: NSLocalizedString("UpdateHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonUpdateHandler.rawValue, + configHandler: configureUpdateHandlerButton), + CaseElement(title: NSLocalizedString("UpdateImageHandlerTitle", bundle: .module, comment: ""), + cellID: ButtonKind.buttonImageUpdateHandler.rawValue, + configHandler: configureUpdateImageHandlerButton) + ]) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CaseElement.swift b/BenchmarkTests/UIKitCatalog/CaseElement.swift new file mode 100644 index 0000000000..54f0ebdf74 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CaseElement.swift @@ -0,0 +1,29 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Test case element that serves our UITableViewCells. +*/ + +import UIKit + +struct CaseElement { + var title: String // Visual title of the cell (table section header title) + var cellID: String // Table view cell's identifier for searching for the cell within the nib file. + + typealias ConfigurationClosure = (UIView) -> Void + var configHandler: ConfigurationClosure // Configuration handler for setting up the cell's subview. + + init(title: String, cellID: String, configHandler: @escaping (V) -> Void) { + self.title = title + self.cellID = cellID + self.configHandler = { view in + guard let view = view as? V else { fatalError("Impossible") } + configHandler(view) + } + } + + func targetView(_ cell: UITableViewCell?) -> UIView? { + return cell != nil ? cell!.contentView.subviews[0] : nil + } +} diff --git a/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift b/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift new file mode 100755 index 0000000000..77838319a0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ColorPickerViewController.swift @@ -0,0 +1,144 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIColorPickerViewController`. +*/ + +import UIKit + +class ColorPickerViewController: UIViewController, UIColorPickerViewControllerDelegate { + + // MARK: - Properties + + var colorWell: UIColorWell! + var colorPicker: UIColorPickerViewController! + + @IBOutlet var pickerButton: UIButton! // UIButton to present the picker. + @IBOutlet var pickerWellView: UIView! // UIView placeholder to hold the UIColorWell. + + @IBOutlet var colorView: UIView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureColorPicker() + configureColorWell() + + // For iOS, the picker button in the main view is not used, the color picker is presented from the navigation bar. + if navigationController?.traitCollection.userInterfaceIdiom != .mac { + pickerButton.isHidden = true + } + } + + // MARK: - UIColorWell + + // Update the color view from the color well chosen action. + func colorWellHandler(action: UIAction) { + if let colorWell = action.sender as? UIColorWell { + colorView.backgroundColor = colorWell.selectedColor + } + } + + func configureColorWell() { + + /** Note: Both color well and picker buttons achieve the same thing, presenting the color picker. + But one presents it with a color well control, the other by a bar button item. + */ + let colorWellAction = UIAction(title: "", handler: colorWellHandler) + colorWell = + UIColorWell(frame: CGRect(x: 0, y: 0, width: 32, height: 32), primaryAction: colorWellAction) + + // For Mac Catalyst, the UIColorWell is placed in the main view. + if navigationController?.traitCollection.userInterfaceIdiom == .mac { + pickerWellView.addSubview(colorWell) + } else { + // For iOS, the UIColorWell is placed inside the navigation bar as a UIBarButtonItem. + let colorWellBarItem = UIBarButtonItem(customView: colorWell) + let fixedBarItem = UIBarButtonItem.fixedSpace(20.0) + navigationItem.rightBarButtonItems!.append(fixedBarItem) + navigationItem.rightBarButtonItems!.append(colorWellBarItem) + } + } + + // MARK: - UIColorPickerViewController + + func configureColorPicker() { + colorPicker = UIColorPickerViewController() + colorPicker.supportsAlpha = true + colorPicker.selectedColor = UIColor.blue + colorPicker.delegate = self + } + + // Present the color picker from the UIBarButtonItem, iOS only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByBarButton(_ sender: UIBarButtonItem) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover // will display as popover for iPad or sheet for compact screens. + let popover: UIPopoverPresentationController = colorPicker.popoverPresentationController! + popover.barButtonItem = sender + present(colorPicker, animated: true, completion: nil) + } + + // Present the color picker from the UIButton, Mac Catalyst only. + // This will present it as a popover (preferred), or for compact mode as a modal sheet. + @IBAction func presentColorPickerByButton(_ sender: UIButton) { + colorPicker.modalPresentationStyle = UIModalPresentationStyle.popover + if let popover = colorPicker.popoverPresentationController { + popover.sourceView = sender + present(colorPicker, animated: true, completion: nil) + } + } + + // MARK: - UIColorPickerViewControllerDelegate + + // Color returned from the color picker via UIBarButtonItem - iOS 15.0 + @available(iOS 15.0, *) + func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Dismiss the color picker if the conditions are right: + // 1) User is not doing a continous pick (tap and drag across multiple colors). + // 2) Picker is presented on a non-compact device. + // + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if !continuously { + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + } + + // Color returned from the color picker - iOS 14.x and earlier. + func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { + // User has chosen a color. + let chosenColor = viewController.selectedColor + colorView.backgroundColor = chosenColor + + // Use the following check to determine how the color picker was presented (modal or popover). + // For popover, we want to dismiss it when a color is locked. + // For modal, the picker has a close button. + // + if traitCollection.horizontalSizeClass != .compact { + viewController.dismiss(animated: true, completion: { + Swift.debugPrint("\(chosenColor)") + }) + } + } + + func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { + /** In presentations (except popovers) the color picker shows a close button. If the close button is tapped, + the view controller is dismissed and `colorPickerViewControllerDidFinish:` is called. Can be used to + animate alongside the dismissal. + */ + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift b/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift new file mode 100755 index 0000000000..8111b05dea --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomPageControlViewController.swift @@ -0,0 +1,92 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a customized `UIPageControl`. +*/ + +import UIKit + +class CustomPageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var pageControl: UIPageControl! + + @IBOutlet weak var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple, + UIColor.systemGray2, + UIColor.systemGray3, + UIColor.systemGray4, + UIColor.systemGray5 + ] + + let images = [ + UIImage(systemName: "square.fill"), + UIImage(systemName: "square"), + UIImage(systemName: "triangle.fill"), + UIImage(systemName: "triangle"), + UIImage(systemName: "circle.fill"), + UIImage(systemName: "circle"), + UIImage(systemName: "star.fill"), + UIImage(systemName: "star"), + UIImage(systemName: "staroflife"), + UIImage(systemName: "staroflife.fill"), + UIImage(systemName: "heart.fill"), + UIImage(systemName: "heart"), + UIImage(systemName: "moon"), + UIImage(systemName: "moon.fill") + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + // Prominent background style. + pageControl.backgroundStyle = .prominent + + // Set custom indicator images. + for (index, image) in images.enumerated() { + pageControl.setIndicatorImage(image, forPage: index) + } + + pageControl.addTarget(self, + action: #selector(PageControlViewController.pageControlValueDidChange), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift b/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift new file mode 100755 index 0000000000..bfd7738144 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomSearchBarViewController.swift @@ -0,0 +1,61 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UISearchBar`. +*/ + +import UIKit + +class CustomSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsBookmarkButton = true + + searchBar.tintColor = UIColor.systemPurple + + searchBar.backgroundImage = UIImage(named: "search_bar_background", in: .module, compatibleWith: nil) + + // Set the bookmark image for both normal and highlighted states. + let bookImage = UIImage(systemName: "bookmark") + searchBar.setImage(bookImage, for: .bookmark, state: .normal) + + let bookFillImage = UIImage(systemName: "bookmark.fill") + searchBar.setImage(bookFillImage, for: .bookmark, state: .highlighted) + } +} + +// MARK: - UISearchBarDelegate + +extension CustomSearchBarViewController: UISearchBarDelegate { + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar keyboard \"Search\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom search bar \"Cancel\" button was tapped.") + + searchBar.resignFirstResponder() + } + + func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The custom \"bookmark button\" inside the search bar was tapped.") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift new file mode 100755 index 0000000000..df91bffc4d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/CustomToolbarViewController.swift @@ -0,0 +1,72 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UIToolbar`. +*/ + +import UIKit + +class CustomToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarBackgroundImage = UIImage(named: "toolbar_background", in: .module, compatibleWith: nil) + toolbar.setBackgroundImage(toolbarBackgroundImage, forToolbarPosition: .bottom, barMetrics: .default) + + let toolbarButtonItems = [ + customImageBarButtonItem, + flexibleSpaceBarButtonItem, + customBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var customImageBarButtonItem: UIBarButtonItem { + let customBarButtonItemImage = UIImage(systemName: "exclamationmark.triangle") + + let customImageBarButtonItem = UIBarButtonItem(image: customBarButtonItemImage, + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) + + customImageBarButtonItem.tintColor = UIColor.systemPurple + + return customImageBarButtonItem + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + } + + var customBarButtonItem: UIBarButtonItem { + let barButtonItem = UIBarButtonItem(title: NSLocalizedString("Button", bundle: .module, comment: ""), + style: .plain, + target: self, + action: #selector(CustomToolbarViewController.barButtonItemClicked)) + + let attributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple + ] + barButtonItem.setTitleTextAttributes(attributes, for: []) + + return barButtonItem + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the custom toolbar was clicked: \(barButtonItem).") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/DatePickerController.swift b/BenchmarkTests/UIKitCatalog/DatePickerController.swift new file mode 100755 index 0000000000..464e479a83 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DatePickerController.swift @@ -0,0 +1,82 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIDatePicker`. +*/ + +import UIKit + +class DatePickerController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var datePicker: UIDatePicker! + + @IBOutlet weak var dateLabel: UILabel! + + // A date formatter to format the `date` property of `datePicker`. + lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + return dateFormatter + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 15, *) { + // In case the label's content is too large to fit inside the label (causing truncation), + // use this to reveal the label's full text drawn as a tool tip. + dateLabel.showsExpansionTextWhenTruncated = true + } + + configureDatePicker() + } + + // MARK: - Configuration + + func configureDatePicker() { + datePicker.datePickerMode = .dateAndTime + + /** Set min/max date for the date picker. As an example we will limit the date between + now and 7 days from now. + */ + let now = Date() + datePicker.minimumDate = now + + // Decide the best date picker style based on the trait collection's vertical size. + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + + var dateComponents = DateComponents() + dateComponents.day = 7 + + let sevenDaysFromNow = Calendar.current.date(byAdding: .day, value: 7, to: now) + datePicker.maximumDate = sevenDaysFromNow + + datePicker.minuteInterval = 2 + + datePicker.addTarget(self, action: #selector(DatePickerController.updateDatePickerLabel), for: .valueChanged) + + updateDatePickerLabel() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + // Adjust the date picker style due to the trait collection's vertical size. + super.traitCollectionDidChange(previousTraitCollection) + datePicker.preferredDatePickerStyle = traitCollection.verticalSizeClass == .compact ? .compact : .inline + } + + // MARK: - Actions + + @objc + func updateDatePickerLabel() { + dateLabel.text = dateFormatter.string(from: datePicker.date) + + Swift.debugPrint("Chosen date: \(dateFormatter.string(from: datePicker.date))") + } +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift new file mode 100755 index 0000000000..42f66cf414 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultPageControlViewController.swift @@ -0,0 +1,62 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIPageControl`. +*/ + +import UIKit + +class PageControlViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var pageControl: UIPageControl! + + @IBOutlet weak var colorView: UIView! + + // Colors that correspond to the selected page. Used as the background color for `colorView`. + let colors = [ + UIColor.black, + UIColor.systemGray, + UIColor.systemRed, + UIColor.systemGreen, + UIColor.systemBlue, + UIColor.systemPink, + UIColor.systemYellow, + UIColor.systemIndigo, + UIColor.systemOrange, + UIColor.systemPurple + ] + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePageControl() + pageControlValueDidChange() + } + + // MARK: - Configuration + + func configurePageControl() { + // The total number of available pages is based on the number of available colors. + pageControl.numberOfPages = colors.count + pageControl.currentPage = 2 + + pageControl.pageIndicatorTintColor = UIColor.systemGreen + pageControl.currentPageIndicatorTintColor = UIColor.systemPurple + + pageControl.addTarget(self, action: #selector(PageControlViewController.pageControlValueDidChange), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func pageControlValueDidChange() { + // Note: gesture swiping between pages is provided by `UIPageViewController` and not `UIPageControl`. + Swift.debugPrint("The page control changed its current page to \(pageControl.currentPage).") + + colorView.backgroundColor = colors[pageControl.currentPage] + } +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift new file mode 100755 index 0000000000..cd0d9be1c1 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultSearchBarViewController.swift @@ -0,0 +1,56 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a default `UISearchBar`. +*/ + +import UIKit + +class DefaultSearchBarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var searchBar: UISearchBar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureSearchBar() + } + + // MARK: - Configuration + + func configureSearchBar() { + searchBar.showsCancelButton = true + searchBar.showsScopeBar = true + + searchBar.scopeButtonTitles = [ + NSLocalizedString("Scope One", bundle: .module, comment: ""), + NSLocalizedString("Scope Two", bundle: .module, comment: "") + ] + } + +} + +// MARK: - UISearchBarDelegate + +extension DefaultSearchBarViewController: UISearchBarDelegate { + func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + Swift.debugPrint("The default search selected scope button index changed to \(selectedScope).") + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar keyboard search button was tapped: \(String(describing: searchBar.text)).") + + searchBar.resignFirstResponder() + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + Swift.debugPrint("The default search bar cancel button was tapped.") + + searchBar.resignFirstResponder() + } + +} diff --git a/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift new file mode 100755 index 0000000000..5b8717f57a --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/DefaultToolbarViewController.swift @@ -0,0 +1,60 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use a default `UIToolbar`. +*/ + +import UIKit + +class DefaultToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + let toolbarButtonItems = [ + trashBarButtonItem, + flexibleSpaceBarButtonItem, + customTitleBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - UIBarButtonItem Creation and Configuration + + var trashBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .trash, + target: self, + action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + func menuHandler(action: UIAction) { + Swift.debugPrint("Menu Action '\(action.title)'") + } + + var customTitleBarButtonItem: UIBarButtonItem { + let buttonMenu = UIMenu(title: "", + children: (1...5).map { + UIAction(title: "Option \($0)", handler: menuHandler) + }) + return UIBarButtonItem(image: UIImage(systemName: "list.number"), menu: buttonMenu) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the default toolbar was clicked: \(barButtonItem).") + } +} diff --git a/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift b/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift new file mode 100755 index 0000000000..8294fe784d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/FontPickerViewController.swift @@ -0,0 +1,108 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIFontPickerViewController`. +*/ + +import UIKit + +class FontPickerViewController: UIViewController { + + // MARK: - Properties + + var fontPicker: UIFontPickerViewController! + var textFormatter: UITextFormattingCoordinator! + + @IBOutlet var fontLabel: UILabel! + @IBOutlet var textFormatterButton: UIButton! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + fontLabel.text = NSLocalizedString("SampleFontTitle", bundle: .module, comment: "") + + configureFontPicker() + + if traitCollection.userInterfaceIdiom != .mac { + // UITextFormattingCoordinator's toggleFontPanel is available only for macOS. + textFormatterButton.isHidden = true + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + configureTextFormatter() + } + + func configureFontPicker() { + let configuration = UIFontPickerViewController.Configuration() + configuration.includeFaces = true + configuration.displayUsingSystemFont = false + configuration.filteredTraits = [.classModernSerifs] + + fontPicker = UIFontPickerViewController(configuration: configuration) + fontPicker.delegate = self + fontPicker.modalPresentationStyle = UIModalPresentationStyle.popover + } + + func configureTextFormatter() { + if textFormatter == nil { + guard let scene = self.view.window?.windowScene else { return } + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: fontLabel.font as Any] + textFormatter = UITextFormattingCoordinator(for: scene) + textFormatter.delegate = self + textFormatter.setSelectedAttributes(attributes, isMultiple: true) + } + } + + @IBAction func presentFontPicker(_ sender: Any) { + if let button = sender as? UIButton { + let popover: UIPopoverPresentationController = fontPicker.popoverPresentationController! + popover.sourceView = button + present(fontPicker, animated: true, completion: nil) + } + } + + @IBAction func presentTextFormattingCoordinator(_ sender: Any) { + if !UITextFormattingCoordinator.isFontPanelVisible { + UITextFormattingCoordinator.toggleFontPanel(sender) + } + } + +} + +// MARK: - UIFontPickerViewControllerDelegate + +extension FontPickerViewController: UIFontPickerViewControllerDelegate { + + func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) { + //.. + } + + func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) { + guard let fontDescriptor = viewController.selectedFontDescriptor else { return } + let font = UIFont(descriptor: fontDescriptor, size: 28.0) + fontLabel.font = font + } + +} + +// MARK: - UITextFormattingCoordinatorDelegate + +extension FontPickerViewController: UITextFormattingCoordinatorDelegate { + + override func updateTextAttributes(conversionHandler: ([NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any]) { + guard let oldLabelText = fontLabel.attributedText else { return } + let newString = NSMutableAttributedString(string: oldLabelText.string) + oldLabelText.enumerateAttributes(in: NSRange(location: 0, length: oldLabelText.length), + options: []) { (attributeDictionary, range, stop) in + newString.setAttributes(conversionHandler(attributeDictionary), range: range) + } + fontLabel.attributedText = newString + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift b/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift new file mode 100755 index 0000000000..b2bb197f23 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ImagePickerViewController.swift @@ -0,0 +1,45 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIFontPickerViewController`. +*/ + +import UIKit + +class ImagePickerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + // MARK: - Properties + var imagePicker: UIImagePickerController! + @IBOutlet var imageView: UIImageView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImagePicker() + } + + func configureImagePicker() { + imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.mediaTypes = ["public.image"] + imagePicker.sourceType = .photoLibrary + } + + @IBAction func presentImagePicker(_: AnyObject) { + present(imagePicker, animated: true) + } + + // MARK: - UIImagePickerControllerDelegate + + func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + imageView.image = image + } + picker.dismiss(animated: true, completion: nil) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/ImageViewController.swift b/BenchmarkTests/UIKitCatalog/ImageViewController.swift new file mode 100755 index 0000000000..4abc247509 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ImageViewController.swift @@ -0,0 +1,44 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIImageView`. +*/ + +import UIKit + +class ImageViewController: UIViewController { + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureImageView() + } + + // MARK: - Configuration + + func configureImageView() { + // The root view of the view controller is set in Interface Builder and is an UIImageView. + if let imageView = view as? UIImageView { + // Fetch the images (each image is of the format Flowers_number). + imageView.animationImages = (1...2).map { UIImage(named: "Flowers_\($0)", in: .module, compatibleWith: nil)! } + + // We want the image to be scaled to the correct aspect ratio within imageView's bounds. + imageView.contentMode = .scaleAspectFit + + imageView.animationDuration = 5 + imageView.startAnimating() + + imageView.isAccessibilityElement = true + imageView.accessibilityLabel = NSLocalizedString("Animated", bundle: .module, comment: "") + + if #available(iOS 15, *) { + // This case uses UIToolTipInteraction which is available on iOS 15 or later. + let interaction = + UIToolTipInteraction(defaultToolTip: NSLocalizedString("ImageToolTipTitle", bundle: .module, comment: "")) + imageView.addInteraction(interaction) + } + } + } +} diff --git a/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt b/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt new file mode 100755 index 0000000000..1f0d0578f9 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/LICENSE/LICENSE.txt @@ -0,0 +1,8 @@ +Copyright © 2021 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift b/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift new file mode 100755 index 0000000000..35c3b10e37 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/MenuButtonViewController.swift @@ -0,0 +1,184 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to attach menus to `UIButton`. +*/ + +import UIKit + +class MenuButtonViewController: BaseTableViewController { + + // Cell identifier for each menu button table view cell. + enum MenuButtonKind: String, CaseIterable { + case buttonMenuProgrammatic + case buttonMenuMultiAction + case buttonSubMenu + case buttonMenuSelection + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DropDownProgTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuProgrammatic.rawValue, + configHandler: configureDropDownProgrammaticButton), + CaseElement(title: NSLocalizedString("DropDownMultiActionTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuMultiAction.rawValue, + configHandler: configureDropdownMultiActionButton), + CaseElement(title: NSLocalizedString("DropDownButtonSubMenuTitle", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonSubMenu.rawValue, + configHandler: configureDropdownSubMenuButton), + CaseElement(title: NSLocalizedString("PopupSelection", bundle: .module, comment: ""), + cellID: MenuButtonKind.buttonMenuSelection.rawValue, + configHandler: configureSelectionPopupButton) + ]) + } + + // MARK: - Handlers + + enum ButtonMenuActionIdentifiers: String { + case item1 + case item2 + case item3 + } + func menuHandler(action: UIAction) { + switch action.identifier.rawValue { + case ButtonMenuActionIdentifiers.item1.rawValue: + Swift.debugPrint("Menu Action: item 1") + case ButtonMenuActionIdentifiers.item2.rawValue: + Swift.debugPrint("Menu Action: item 2") + case ButtonMenuActionIdentifiers.item3.rawValue: + Swift.debugPrint("Menu Action: item 3") + default: break + } + } + + func item4Handler(action: UIAction) { + Swift.debugPrint("Menu Action: \(action.title)") + } + + // MARK: - Drop Down Menu Buttons + + func configureDropDownProgrammaticButton(button: UIButton) { + button.menu = UIMenu(children: [ + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "1"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "2"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler) + ]) + + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownMultiActionButton(button: UIButton) { + let buttonMenu = UIMenu(children: [ + // Share a single handler for the first 3 actions. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "1"), + image: UIImage(systemName: "1.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item1.rawValue), + attributes: [], + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "2"), + image: UIImage(systemName: "2.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item2.rawValue), + handler: menuHandler), + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "3"), + image: UIImage(systemName: "3.circle"), + identifier: UIAction.Identifier(ButtonMenuActionIdentifiers.item3.rawValue), + handler: menuHandler), + + // Use a separate handler for this 4th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "4"), + image: UIImage(systemName: "4.circle"), + identifier: nil, + handler: item4Handler(action:)), + + // Use a closure for the 5th action. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "5"), + image: UIImage(systemName: "5.circle"), + identifier: nil) { action in + Swift.debugPrint("Menu Action: \(action.title)") + }, + + // Use attributes to make the 6th action disabled. + UIAction(title: String(format: NSLocalizedString("ItemTitle", bundle: .module, comment: ""), "6"), + image: UIImage(systemName: "6.circle"), + identifier: nil, + attributes: [UIMenuElement.Attributes.disabled]) { action in + Swift.debugPrint("Menu Action: \(action.title)") + } + ]) + button.menu = buttonMenu + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + } + + func configureDropdownSubMenuButton(button: UIButton) { + let sortClosure = { (action: UIAction) in + Swift.debugPrint("Sort by: \(action.title)") + } + let refreshClosure = { (action: UIAction) in + Swift.debugPrint("Refresh handler") + } + let accountHandler = { (action: UIAction) in + Swift.debugPrint("Account handler") + } + + var sortMenu: UIMenu + if #available(iOS 15, *) { // .singleSelection option only on iOS 15 or later + // The sort sub menu supports a selection. + sortMenu = UIMenu(title: "Sort By", options: .singleSelection, children: [ + UIAction(title: "Date", state: .on, handler: sortClosure), + UIAction(title: "Size", handler: sortClosure) + ]) + } else { + sortMenu = UIMenu(title: "Sort By", children: [ + UIAction(title: "Date", handler: sortClosure), + UIAction(title: "Size", handler: sortClosure) + ]) + } + + let topMenu = UIMenu(children: [ + UIAction(title: "Refresh", handler: refreshClosure), + UIAction(title: "Account", handler: accountHandler), + sortMenu + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + button.menu = topMenu + } + + // MARK: - Selection Popup Menu Button + + func updateColor(_ title: String) { + Swift.debugPrint("Color selected: \(title)") + } + + func configureSelectionPopupButton(button: UIButton) { + let colorClosure = { [unowned self] (action: UIAction) in + self.updateColor(action.title) + } + + button.menu = UIMenu(children: [ + UIAction(title: "Red", handler: colorClosure), + UIAction(title: "Green", state: .on, handler: colorClosure), // The default selected item (green). + UIAction(title: "Blue", handler: colorClosure) + ]) + + // This makes the button behave like a drop down menu. + button.showsMenuAsPrimaryAction = true + + if #available(iOS 15, *) { + button.changesSelectionAsPrimaryAction = true + // Select the default menu item (green). + updateColor((button.menu?.selectedElements.first!.title)!) + } + } + +} diff --git a/DatadogCore/Sources/Utils/Globals.swift b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift similarity index 59% rename from DatadogCore/Sources/Utils/Globals.swift rename to BenchmarkTests/UIKitCatalog/ModuleBundle.swift index baeb96d304..9821bc2283 100644 --- a/DatadogCore/Sources/Utils/Globals.swift +++ b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift @@ -6,9 +6,10 @@ import Foundation -#if SPM_BUILD -import DatadogPrivate -#endif +private class ModuleClass { } -/// Exception handler rethrowing `NSExceptions` to Swift `NSError`. -internal var objcExceptionHandler = __dd_private_ObjcExceptionHandler() +extension Bundle { + static var module: Bundle { Bundle(for: ModuleClass.self) } +} + +public let bundle: Bundle = .module diff --git a/BenchmarkTests/UIKitCatalog/OutlineViewController.swift b/BenchmarkTests/UIKitCatalog/OutlineViewController.swift new file mode 100755 index 0000000000..41801645e8 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/OutlineViewController.swift @@ -0,0 +1,336 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A simple outline view for the sample app's main UI +*/ + +import UIKit + +class OutlineViewController: UIViewController { + + enum Section { + case main + } + + class OutlineItem: Identifiable, Hashable { + let title: String + let subitems: [OutlineItem] + let storyboardName: String? + let imageName: String? + + init(title: String, imageName: String?, storyboardName: String? = nil, subitems: [OutlineItem] = []) { + self.title = title + self.subitems = subitems + self.storyboardName = storyboardName + self.imageName = imageName + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool { + return lhs.id == rhs.id + } + + } + + var dataSource: UICollectionViewDiffableDataSource! = nil + var outlineCollectionView: UICollectionView! = nil + + private var detailTargetChangeObserver: Any? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + configureCollectionView() + configureDataSource() + + // Add a translucent background to the primary view controller for the Mac. + splitViewController!.primaryBackgroundStyle = .sidebar + view.backgroundColor = UIColor.clear + + // Listen for when the split view controller is expanded or collapsed for iPad multi-tasking, + // and on device rotate (iPhones that support regular size class). + detailTargetChangeObserver = + NotificationCenter.default.addObserver(forName: UIViewController.showDetailTargetDidChangeNotification, + object: nil, + queue: OperationQueue.main, + using: { _ in + // Posted when a split view controller is expanded or collapsed. + + // Re-load the data source, the disclosure indicators need to change (push vs. present on a cell). + var snapshot = self.dataSource.snapshot() + snapshot.reloadItems(self.menuItems) + self.dataSource.apply(snapshot, animatingDifferences: false) + }) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navigationController!.navigationBar.isHidden = true + } + } + + deinit { + if let observer = detailTargetChangeObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + lazy var controlsOutlineItem: OutlineItem = { + + // Determine the content of the UIButton grouping. + var buttonItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", bundle: .module, comment: ""), imageName: "rectangle", + storyboardName: "ButtonViewController"), + OutlineItem(title: NSLocalizedString("MenuButtonsTitle", bundle: .module, comment: ""), imageName: "list.bullet.rectangle", + storyboardName: "MenuButtonViewController") + ] + // UIPointerInteraction to UIButtons is applied for iPad. + if navigationController!.traitCollection.userInterfaceIdiom == .pad { + buttonItems.append(contentsOf: + [OutlineItem(title: NSLocalizedString("PointerInteractionButtonsTitle", bundle: .module, comment: ""), + imageName: "cursorarrow.rays", + storyboardName: "PointerInteractionButtonViewController") ]) + } + + var controlsSubItems = [ + OutlineItem(title: NSLocalizedString("ButtonsTitle", bundle: .module, comment: ""), imageName: "rectangle.on.rectangle", subitems: buttonItems), + + OutlineItem(title: NSLocalizedString("PageControlTitle", bundle: .module, comment: ""), imageName: "photo.on.rectangle", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultPageControlTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultPageControlViewController"), + OutlineItem(title: NSLocalizedString("CustomPageControlTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomPageControlViewController") + ]), + + OutlineItem(title: NSLocalizedString("SearchBarsTitle", bundle: .module, comment: ""), imageName: "magnifyingglass", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultSearchBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultSearchBarViewController"), + OutlineItem(title: NSLocalizedString("CustomSearchBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomSearchBarViewController") + ]), + + OutlineItem(title: NSLocalizedString("SegmentedControlsTitle", bundle: .module, comment: ""), imageName: "square.split.3x1", + storyboardName: "SegmentedControlViewController"), + OutlineItem(title: NSLocalizedString("SlidersTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SliderViewController"), + OutlineItem(title: NSLocalizedString("SwitchesTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SwitchViewController"), + OutlineItem(title: NSLocalizedString("TextFieldsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TextFieldViewController") + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIStepper class is not supported when running Mac Catalyst apps in the Mac idiom. + let stepperItem = + OutlineItem(title: NSLocalizedString("SteppersTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "StepperViewController") + controlsSubItems.append(stepperItem) + } + + return OutlineItem(title: "Controls", imageName: "slider.horizontal.3", subitems: controlsSubItems) + }() + + lazy var pickersOutlineItem: OutlineItem = { + var pickerSubItems = [ + OutlineItem(title: NSLocalizedString("DatePickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DatePickerController"), + OutlineItem(title: NSLocalizedString("ColorPickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ColorPickerViewController"), + OutlineItem(title: NSLocalizedString("FontPickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "FontPickerViewController"), + OutlineItem(title: NSLocalizedString("ImagePickerTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ImagePickerViewController") + ] + + if traitCollection.userInterfaceIdiom != .mac { + // UIPickerView class is not supported when running Mac Catalyst apps in the Mac idiom. + // To use a picker in macOS, use UIButton with changesSelectionAsPrimaryAction set to "true". + let pickerViewItem = + OutlineItem(title: NSLocalizedString("PickerViewTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "PickerViewController") + pickerSubItems.append(pickerViewItem) + } + + return OutlineItem(title: "Pickers", imageName: "list.bullet", subitems: pickerSubItems) + }() + + lazy var viewsOutlineItem: OutlineItem = { + OutlineItem(title: "Views", imageName: "rectangle.stack.person.crop", subitems: [ + OutlineItem(title: NSLocalizedString("ActivityIndicatorsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ActivityIndicatorViewController"), + OutlineItem(title: NSLocalizedString("AlertControllersTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "AlertControllerViewController"), + OutlineItem(title: NSLocalizedString("TextViewTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TextViewController"), + + OutlineItem(title: NSLocalizedString("ImagesTitle", bundle: .module, comment: ""), imageName: "photo", subitems: [ + OutlineItem(title: NSLocalizedString("ImageViewTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ImageViewController"), + OutlineItem(title: NSLocalizedString("SymbolsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "SymbolViewController") + ]), + + OutlineItem(title: NSLocalizedString("ProgressViewsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "ProgressViewController"), + OutlineItem(title: NSLocalizedString("StackViewsTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "StackViewController"), + + OutlineItem(title: NSLocalizedString("ToolbarsTitle", bundle: .module, comment: ""), imageName: "hammer", subitems: [ + OutlineItem(title: NSLocalizedString("DefaultToolBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "DefaultToolbarViewController"), + OutlineItem(title: NSLocalizedString("TintedToolbarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "TintedToolbarViewController"), + OutlineItem(title: NSLocalizedString("CustomToolbarBarTitle", bundle: .module, comment: ""), imageName: nil, + storyboardName: "CustomToolbarViewController") + ]), + + OutlineItem(title: NSLocalizedString("VisualEffectTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "VisualEffectViewController"), + + OutlineItem(title: NSLocalizedString("WebViewTitle", bundle: .module, comment: ""), imageName: nil, storyboardName: "WebViewController") + ]) + }() + + private lazy var menuItems: [OutlineItem] = { + return [ + controlsOutlineItem, + viewsOutlineItem, + pickersOutlineItem + ] + }() + +} + +// MARK: - UICollectionViewDiffableDataSource + +extension OutlineViewController { + + private func configureCollectionView() { + let collectionView = + UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout()) + view.addSubview(collectionView) + collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + self.outlineCollectionView = collectionView + collectionView.delegate = self + } + + private func configureDataSource() { + + let containerCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, menuItem) in + + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline) + cell.contentConfiguration = contentConfiguration + + let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header) + cell.accessories = [.outlineDisclosure(options: disclosureOptions)] + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + } + + let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, menuItem in + var contentConfiguration = cell.defaultContentConfiguration() + contentConfiguration.text = menuItem.title + + if let image = menuItem.imageName { + contentConfiguration.image = UIImage(systemName: image) + } + + cell.contentConfiguration = contentConfiguration + + let background = UIBackgroundConfiguration.clear() + cell.backgroundConfiguration = background + + cell.accessories = self.splitViewWantsToShowDetail() ? [] : [.disclosureIndicator()] + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: outlineCollectionView) { + (collectionView: UICollectionView, indexPath: IndexPath, item: OutlineItem) -> UICollectionViewCell? in + // Return the cell. + if item.subitems.isEmpty { + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } else { + return collectionView.dequeueConfiguredReusableCell(using: containerCellRegistration, for: indexPath, item: item) + } + } + + // Load our initial data. + let snapshot = initialSnapshot() + self.dataSource.apply(snapshot, to: .main, animatingDifferences: false) + } + + private func generateLayout() -> UICollectionViewLayout { + let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar) + let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration) + return layout + } + + private func initialSnapshot() -> NSDiffableDataSourceSectionSnapshot { + var snapshot = NSDiffableDataSourceSectionSnapshot() + + func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) { + snapshot.append(menuItems, to: parent) + for menuItem in menuItems where !menuItem.subitems.isEmpty { + addItems(menuItem.subitems, to: menuItem) + } + } + + addItems(menuItems, to: nil) + return snapshot + } + +} + +// MARK: - UICollectionViewDelegate + +extension OutlineViewController: UICollectionViewDelegate { + + private func splitViewWantsToShowDetail() -> Bool { + return splitViewController?.traitCollection.horizontalSizeClass == .regular + } + + private func pushOrPresentViewController(viewController: UIViewController) { + if splitViewWantsToShowDetail() { + let navVC = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(navVC, sender: navVC) // Replace the detail view controller. + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + navVC.navigationBar.isHidden = true + } + } else { + navigationController?.pushViewController(viewController, animated: true) // Just push instead of replace. + } + } + + private func pushOrPresentStoryboard(storyboardName: String) { + let exampleStoryboard = UIStoryboard(name: storyboardName, bundle: .module) + if let exampleViewController = exampleStoryboard.instantiateInitialViewController() { + pushOrPresentViewController(viewController: exampleViewController) + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let menuItem = self.dataSource.itemIdentifier(for: indexPath) else { return } + + collectionView.deselectItem(at: indexPath, animated: true) + + if let storyboardName = menuItem.storyboardName { + pushOrPresentStoryboard(storyboardName: storyboardName) + + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + if let windowScene = view.window?.windowScene { + if #available(iOS 15, *) { + windowScene.subtitle = menuItem.title + } + } + } + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/PickerViewController.swift b/BenchmarkTests/UIKitCatalog/PickerViewController.swift new file mode 100755 index 0000000000..a4bd6bffcd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/PickerViewController.swift @@ -0,0 +1,171 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIPickerView`. +*/ + +import UIKit + +class PickerViewController: UIViewController { + // MARK: - Types + + enum ColorComponent: Int { + case red = 0, green, blue + + static var count: Int { + return ColorComponent.blue.rawValue + 1 + } + } + + struct RGB { + static let max: CGFloat = 255.0 + static let min: CGFloat = 0.0 + static let offset: CGFloat = 5.0 + } + + // MARK: - Properties + + @IBOutlet weak var pickerView: UIPickerView! + @IBOutlet weak var colorSwatchView: UIView! + + lazy var numberOfColorValuesPerComponent: Int = (Int(RGB.max) / Int(RGB.offset)) + 1 + + var redColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var greenColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + var blueColor: CGFloat = RGB.min { + didSet { + updateColorSwatchViewBackgroundColor() + } + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configurePickerView() + } + + func updateColorSwatchViewBackgroundColor() { + colorSwatchView.backgroundColor = UIColor(red: redColor, green: greenColor, blue: blueColor, alpha: 1) + } + + func configurePickerView() { + // Set the default selected rows (the desired rows to initially select will vary from app to app). + let selectedRows: [ColorComponent: Int] = [.red: 13, .green: 41, .blue: 24] + + for (colorComponent, selectedRow) in selectedRows { + /** Note that the delegate method on `UIPickerViewDelegate` is not triggered + when manually calling `selectRow(_:inComponent:animated:)`. To do + this, we fire off delegate method manually. + */ + pickerView.selectRow(selectedRow, inComponent: colorComponent.rawValue, animated: true) + pickerView(pickerView, didSelectRow: selectedRow, inComponent: colorComponent.rawValue) + } + } + +} + +// MARK: - UIPickerViewDataSource + +extension PickerViewController: UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return ColorComponent.count + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return numberOfColorValuesPerComponent + } +} + +// MARK: - UIPickerViewDelegate + +extension PickerViewController: UIPickerViewDelegate { + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + let colorValue = CGFloat(row) * RGB.offset + + // Set the initial colors for each picker segment. + let value = CGFloat(colorValue) / RGB.max + var redColorComponent = RGB.min + var greenColorComponent = RGB.min + var blueColorComponent = RGB.min + + switch ColorComponent(rawValue: component)! { + case .red: + redColorComponent = value + + case .green: + greenColorComponent = value + + case .blue: + blueColorComponent = value + } + + if redColorComponent < 0.5 { + redColorComponent = 0.5 + } + if blueColorComponent < 0.5 { + blueColorComponent = 0.5 + } + if greenColorComponent < 0.5 { + greenColorComponent = 0.5 + } + let foregroundColor = UIColor(red: redColorComponent, green: greenColorComponent, blue: blueColorComponent, alpha: 1.0) + + // Set the foreground color for the entire attributed string. + let attributes = [ + NSAttributedString.Key.foregroundColor: foregroundColor + ] + + let title = NSMutableAttributedString(string: "\(Int(colorValue))", attributes: attributes) + + return title + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + let colorComponentValue = RGB.offset * CGFloat(row) / RGB.max + + switch ColorComponent(rawValue: component)! { + case .red: + redColor = colorComponentValue + + case .green: + greenColor = colorComponentValue + + case .blue: + blueColor = colorComponentValue + } + } + +} + +// MARK: - UIPickerViewAccessibilityDelegate + +extension PickerViewController: UIPickerViewAccessibilityDelegate { + + func pickerView(_ pickerView: UIPickerView, accessibilityLabelForComponent component: Int) -> String? { + + switch ColorComponent(rawValue: component)! { + case .red: + return NSLocalizedString("Red color component value", bundle: .module, comment: "") + + case .green: + return NSLocalizedString("Green color component value", bundle: .module, comment: "") + + case .blue: + return NSLocalizedString("Blue color component value", bundle: .module, comment: "") + } + } +} + diff --git a/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift b/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift new file mode 100755 index 0000000000..b9283464c0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/PointerInteractionButtonViewController.swift @@ -0,0 +1,168 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to intergrate pointer interactions to `UIButton`. +*/ + +import UIKit + +class PointerInteractionButtonViewController: BaseTableViewController { + + // Cell identifier for each button pointer table view cell. + enum PointerButtonKind: String, CaseIterable { + case buttonPointer + case buttonHighlight + case buttonLift + case buttonHover + case buttonCustom + } + + // The pointer effect kind to use for each button (corresponds to the button's view tag). + enum ButtonPointerEffectKind: Int { + case pointer = 1 + case highlight + case lift + case hover + case custom + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: "UIPointerEffect.automatic", + cellID: PointerButtonKind.buttonPointer.rawValue, + configHandler: configurePointerButton), + CaseElement(title: "UIPointerEffect.highlight", + cellID: PointerButtonKind.buttonHighlight.rawValue, + configHandler: configureHighlightButton), + CaseElement(title: "UIPointerEffect.lift", + cellID: PointerButtonKind.buttonLift.rawValue, + configHandler: configureLiftButton), + CaseElement(title: "UIPointerEffect.hover", + cellID: PointerButtonKind.buttonHover.rawValue, + configHandler: configureHoverButton), + CaseElement(title: "UIPointerEffect (custom)", + cellID: PointerButtonKind.buttonCustom.rawValue, + configHandler: configureCustomButton) + ]) + } + + // MARK: - Configurations + + func configurePointerButton(button: UIButton) { + button.pointerStyleProvider = defaultButtonProvider + } + + func configureHighlightButton(button: UIButton) { + button.pointerStyleProvider = highlightButtonProvider + } + + func configureLiftButton(button: UIButton) { + button.pointerStyleProvider = liftButtonProvider + } + + func configureHoverButton(button: UIButton) { + button.pointerStyleProvider = hoverButtonProvider + } + + func configureCustomButton(button: UIButton) { + button.pointerStyleProvider = customButtonProvider + } + + // MARK: Button Pointer Providers + + func defaultButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** UIPointerEffect.automatic attempts to determine the appropriate effect for the given preview automatically. + The pointer effect has an automatic nature which adapts to the aspects of the button (background color, corner radius, size) + */ + let buttonPointerEffect = UIPointerEffect.automatic(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: pointerShape) + return buttonPointerStyle + } + + func highlightButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + // Pointer slides under the given view and morphs into the view's shape. + let buttonHighlightPointerEffect = UIPointerEffect.highlight(targetedPreview) + buttonPointerStyle = UIPointerStyle(effect: buttonHighlightPointerEffect, shape: pointerShape) + + return buttonPointerStyle + } + + func liftButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer slides under the given view and disappears as the view scales up and gains a shadow. + Make the pointer shape’s bounds match the view’s frame so the highlight extends to the edges. + */ + let buttonLiftPointerEffect = UIPointerEffect.lift(targetedPreview) + let customPointerShape = UIPointerShape.path(UIBezierPath(roundedRect: button.bounds, cornerRadius: 6.0)) + buttonPointerStyle = UIPointerStyle(effect: buttonLiftPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + func hoverButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + // Use the pointer effect's preview that's passed in. + let targetedPreview = pointerEffect.preview + + /** Pointer retains the system shape while over the given view. + Visual changes applied to the view are dictated by the effect's properties. + */ + let buttonHoverPointerEffect = + UIPointerEffect.hover(targetedPreview, preferredTintMode: .none, prefersShadow: true) + buttonPointerStyle = UIPointerStyle(effect: buttonHoverPointerEffect, shape: nil) + + return buttonPointerStyle + } + + func customButtonProvider(button: UIButton, pointerEffect: UIPointerEffect, pointerShape: UIPointerShape) -> UIPointerStyle? { + var buttonPointerStyle: UIPointerStyle? = nil + + /** Hover pointer with a custom triangle pointer shape. + Override the default UITargetedPreview with our own, make the visible path outset a little larger. + */ + let parameters = UIPreviewParameters() + parameters.visiblePath = UIBezierPath(rect: button.bounds.insetBy(dx: -15.0, dy: -15.0)) + let newTargetedPreview = UITargetedPreview(view: button, parameters: parameters) + + let buttonPointerEffect = + UIPointerEffect.hover(newTargetedPreview, preferredTintMode: .overlay, prefersShadow: false, prefersScaledContent: false) + + let customPointerShape = UIPointerShape.path(trianglePointerShape()) + buttonPointerStyle = UIPointerStyle(effect: buttonPointerEffect, shape: customPointerShape) + + return buttonPointerStyle + } + + // Return a triangle bezier path for the pointer's shape. + func trianglePointerShape() -> UIBezierPath { + let width = 20.0 + let height = 20.0 + let offset = 10.0 // Coordinate location to match up with the coordinate of default pointer shape. + + let pathView = UIBezierPath() + pathView.move(to: CGPoint(x: (width / 2) - offset, y: -offset)) + pathView.addLine(to: CGPoint(x: -offset, y: height - offset)) + pathView.addLine(to: CGPoint(x: width - offset, y: height - offset)) + pathView.close() + + return pathView + } +} diff --git a/BenchmarkTests/UIKitCatalog/ProgressViewController.swift b/BenchmarkTests/UIKitCatalog/ProgressViewController.swift new file mode 100755 index 0000000000..04b0b9dcbd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ProgressViewController.swift @@ -0,0 +1,132 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIProgressView`. +*/ + +import UIKit + +class ProgressViewController: BaseTableViewController { + // Cell identifier for each progress view table view cell. + enum ProgressViewKind: String, CaseIterable { + case defaultProgress + case barProgress + case tintedProgress + } + + // MARK: - Properties + + var observer: NSKeyValueObservation? + + // An `NSProgress` object whose `fractionCompleted` is observed using KVO to update the `UIProgressView`s' `progress` properties. + let progress = Progress(totalUnitCount: 10) + + // A repeating timer that, when fired, updates the `NSProgress` object's `completedUnitCount` property. + var updateTimer: Timer? + + var progressViews = [UIProgressView]() // Accumulated progress views from all table cells for progress updating. + + // MARK: - Initialization + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + // Register as an observer of the `NSProgress`'s `fractionCompleted` property. + observer = progress.observe(\.fractionCompleted, options: [.new]) { (_, _) in + // Update the progress views. + for progressView in self.progressViews { + progressView.setProgress(Float(self.progress.fractionCompleted), animated: true) + } + } + } + + deinit { + // Unregister as an observer of the `NSProgress`'s `fractionCompleted` property. + observer?.invalidate() + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressDefaultTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.defaultProgress.rawValue, + configHandler: configureDefaultStyleProgressView), + CaseElement(title: NSLocalizedString("ProgressBarTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.barProgress.rawValue, + configHandler: configureBarStyleProgressView) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + // Tinted progress views available only on iOS. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("ProgressTintedTitle", bundle: .module, comment: ""), + cellID: ProgressViewKind.tintedProgress.rawValue, + configHandler: configureTintedProgressView) + ]) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + /** Reset the `completedUnitCount` of the `NSProgress` object and create + a repeating timer to increment it over time. + */ + progress.completedUnitCount = 0 + + updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (_) in + /** Update the `completedUnitCount` of the `NSProgress` object if it's + not completed. Otherwise, stop the timer. + */ + if self.progress.completedUnitCount < self.progress.totalUnitCount { + self.progress.completedUnitCount += 1 + } else { + self.updateTimer?.invalidate() + } + }) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + // Stop the timer from firing. + updateTimer?.invalidate() + } + + // MARK: - Configuration + + func configureDefaultStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureBarStyleProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .bar + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + + func configureTintedProgressView(_ progressView: UIProgressView) { + progressView.progressViewStyle = .default + + progressView.trackTintColor = UIColor.systemBlue + progressView.progressTintColor = UIColor.systemPurple + + // Reset the completed progress of the `UIProgressView`s. + progressView.setProgress(0.0, animated: false) + + progressViews.append(progressView) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift b/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift new file mode 100755 index 0000000000..c4fe1334bd --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SegmentedControlViewController.swift @@ -0,0 +1,189 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISegmentedControl`. +*/ + +import UIKit + +class SegmentedControlViewController: BaseTableViewController { + + // Cell identifier for each segmented control table view cell. + enum SegmentKind: String, CaseIterable { + case segmentDefault + case segmentTinted + case segmentCustom + case segmentCustomBackground + case segmentAction + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentDefault.rawValue, + configHandler: configureDefaultSegmentedControl), + CaseElement(title: NSLocalizedString("CustomSegmentsTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentCustom.rawValue, + configHandler: configureCustomSegmentsSegmentedControl), + CaseElement(title: NSLocalizedString("CustomBackgroundTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentCustomBackground.rawValue, + configHandler: configureCustomBackgroundSegmentedControl), + CaseElement(title: NSLocalizedString("ActionBasedTitle", bundle: .module, comment: ""), + cellID: SegmentKind.segmentAction.rawValue, + configHandler: configureActionBasedSegmentedControl) + ]) + if self.traitCollection.userInterfaceIdiom != .mac { + // Tinted segmented control is only available on iOS. + testCells.append(contentsOf: [ + CaseElement(title: "Tinted", + cellID: SegmentKind.segmentTinted.rawValue, + configHandler: configureTintedSegmentedControl) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSegmentedControl(_ segmentedControl: UISegmentedControl) { + // As a demonstration, disable the first segment. + segmentedControl.setEnabled(false, forSegmentAt: 0) + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureTintedSegmentedControl(_ segmentedControl: UISegmentedControl) { + // Use a dynamic tinted "green" color (separate one for Light Appearance and separate one for Dark Appearance). + segmentedControl.selectedSegmentTintColor = UIColor(named: "tinted_segmented_control", in: .module, compatibleWith: nil)! + segmentedControl.selectedSegmentIndex = 1 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + func configureCustomSegmentsSegmentedControl(_ segmentedControl: UISegmentedControl) { + let airplaneImage = UIImage(systemName: "airplane") + airplaneImage?.accessibilityLabel = NSLocalizedString("Airplane", bundle: .module, comment: "") + segmentedControl.setImage(airplaneImage, forSegmentAt: 0) + + let giftImage = UIImage(systemName: "gift") + giftImage?.accessibilityLabel = NSLocalizedString("Gift", bundle: .module, comment: "") + segmentedControl.setImage(giftImage, forSegmentAt: 1) + + let burstImage = UIImage(systemName: "burst") + burstImage?.accessibilityLabel = NSLocalizedString("Burst", bundle: .module, comment: "") + segmentedControl.setImage(burstImage, forSegmentAt: 2) + + segmentedControl.selectedSegmentIndex = 0 + + segmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) + } + + // Utility function to resize an image to a particular size. + func scaledImage(_ image: UIImage, scaledToSize newSize: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + image.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let newImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return newImage + } + + // Configure the segmented control with a background image, dividers, and custom font. + // The background image first needs to be sized to match the control's size. + // + func configureCustomBackgroundSegmentedControl(_ placeHolderView: UIView) { + let customBackgroundSegmentedControl = + UISegmentedControl(items: [NSLocalizedString("CheckTitle", bundle: .module, comment: ""), + NSLocalizedString("SearchTitle", bundle: .module, comment: ""), + NSLocalizedString("ToolsTitle", bundle: .module, comment: "")]) + customBackgroundSegmentedControl.selectedSegmentIndex = 2 + + // Place this custom segmented control within the placeholder view. + customBackgroundSegmentedControl.frame.size.width = placeHolderView.frame.size.width + customBackgroundSegmentedControl.frame.origin.y = + (placeHolderView.bounds.size.height - customBackgroundSegmentedControl.bounds.size.height) / 2 + placeHolderView.addSubview(customBackgroundSegmentedControl) + + // Set the background images for each control state. + let normalSegmentBackgroundImage = UIImage(named: "background", in: .module, compatibleWith: nil) + // Size the background image to match the bounds of the segmented control. + let backgroundImageSize = customBackgroundSegmentedControl.bounds.size + let newBackgroundImageSize = scaledImage(normalSegmentBackgroundImage!, scaledToSize: backgroundImageSize) + customBackgroundSegmentedControl.setBackgroundImage(newBackgroundImageSize, for: .normal, barMetrics: .default) + + let disabledSegmentBackgroundImage = UIImage(named: "background_disabled", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setBackgroundImage(disabledSegmentBackgroundImage, for: .disabled, barMetrics: .default) + + let highlightedSegmentBackgroundImage = UIImage(named: "background_highlighted", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setBackgroundImage(highlightedSegmentBackgroundImage, for: .highlighted, barMetrics: .default) + + // Set the divider image. + let segmentDividerImage = UIImage(named: "stepper_and_segment_divider", in: .module, compatibleWith: nil) + customBackgroundSegmentedControl.setDividerImage(segmentDividerImage, + forLeftSegmentState: .normal, + rightSegmentState: .normal, + barMetrics: .default) + + // Create a font to use for the attributed title, for both normal and highlighted states. + let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body), size: 0) + let normalTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemPurple, + NSAttributedString.Key.font: font + ] + customBackgroundSegmentedControl.setTitleTextAttributes(normalTextAttributes, for: .normal) + + let highlightedTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.systemGreen, + NSAttributedString.Key.font: font + ] + customBackgroundSegmentedControl.setTitleTextAttributes(highlightedTextAttributes, for: .highlighted) + + customBackgroundSegmentedControl.addTarget(self, + action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), + for: .valueChanged) + } + + func configureActionBasedSegmentedControl(_ segmentedControl: UISegmentedControl) { + segmentedControl.selectedSegmentIndex = 0 + let firstAction = + UIAction(title: NSLocalizedString("CheckTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(firstAction, forSegmentAt: 0) + let secondAction = + UIAction(title: NSLocalizedString("SearchTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(secondAction, forSegmentAt: 1) + let thirdAction = + UIAction(title: NSLocalizedString("ToolsTitle", bundle: .module, comment: "")) { action in + Swift.debugPrint("Segment Action '\(action.title)'") + } + segmentedControl.setAction(thirdAction, forSegmentAt: 2) + } + + // MARK: - Actions + + @objc + func selectedSegmentDidChange(_ segmentedControl: UISegmentedControl) { + Swift.debugPrint("The selected segment: \(segmentedControl.selectedSegmentIndex).") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID, for: indexPath) + if let segementedControl = cellTest.targetView(cell) as? UISegmentedControl { + cellTest.configHandler(segementedControl) + } else if let placeHolderView = cellTest.targetView(cell) { + // The only non-segmented control cell has a placeholder UIView (for adding one as a subview). + cellTest.configHandler(placeHolderView) + } + return cell + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SliderViewController.swift b/BenchmarkTests/UIKitCatalog/SliderViewController.swift new file mode 100755 index 0000000000..5e24fa16e2 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SliderViewController.swift @@ -0,0 +1,145 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISlider`. +*/ + +import UIKit + +class SliderViewController: BaseTableViewController { + // Cell identifier for each slider table view cell. + enum SliderKind: String, CaseIterable { + case sliderDefault + case sliderTinted + case sliderCustom + case sliderMaxMinImage + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderDefault.rawValue, + configHandler: configureDefaultSlider) + ]) + + if #available(iOS 15, *) { + // These cases require iOS 15 or later when running on Mac Catalyst. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CustomTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderCustom.rawValue, + configHandler: configureCustomSlider) + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("MinMaxImagesTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderMaxMinImage.rawValue, + configHandler: configureMinMaxImageSlider) + ]) + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedTitle", bundle: .module, comment: ""), + cellID: SliderKind.sliderTinted.rawValue, + configHandler: configureTintedSlider) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSlider(_ slider: UISlider) { + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.value = 42 + slider.isContinuous = true + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureTintedSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For minimumTrackTintColor, maximumTrackTintColor to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + slider.minimumTrackTintColor = UIColor.systemBlue + slider.maximumTrackTintColor = UIColor.systemPurple + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + @available(iOS 15.0, *) + func configureCustomSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumTrackImage, setMaximumTrackImage, setThumbImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + + let leftTrackImage = UIImage(named: "slider_blue_track", in: .module, compatibleWith: nil) + slider.setMinimumTrackImage(leftTrackImage, for: .normal) + + let rightTrackImage = UIImage(named: "slider_green_track", in: .module, compatibleWith: nil) + slider.setMaximumTrackImage(rightTrackImage, for: .normal) + + // Set the sliding thumb image (normal and highlighted). + // + // For fun, choose a different image symbol configuraton for the thumb's image between macOS and iOS. + var thumbImageConfig: UIImage.SymbolConfiguration + if slider.traitCollection.userInterfaceIdiom == .mac { + thumbImageConfig = UIImage.SymbolConfiguration(scale: .large) + } else { + thumbImageConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .heavy, scale: .large) + } + let thumbImage = UIImage(systemName: "circle.fill", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImage, for: .normal) + + let thumbImageHighlighted = UIImage(systemName: "circle", withConfiguration: thumbImageConfig) + slider.setThumbImage(thumbImageHighlighted, for: .highlighted) + + // Set the rest of the slider's attributes. + slider.minimumValue = 0 + slider.maximumValue = 100 + slider.isContinuous = false + slider.value = 84 + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + func configureMinMaxImageSlider(slider: UISlider) { + /** To keep the look the same betwen iOS and macOS: + For setMinimumValueImage, setMaximumValueImage to work in Mac Catalyst, use UIBehavioralStyle as ".pad", + Available in macOS 12 or later (Mac Catalyst 15.0 or later). + Use this for controls that need to look the same between iOS and macOS. + */ + if #available(iOS 15, *) { + if traitCollection.userInterfaceIdiom == .mac { + slider.preferredBehavioralStyle = .pad + } + } + + slider.minimumValueImage = UIImage(systemName: "tortoise") + slider.maximumValueImage = UIImage(systemName: "hare") + + slider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func sliderValueDidChange(_ slider: UISlider) { + let formattedValue = String(format: "%.2f", slider.value) + Swift.debugPrint("Slider changed its value: \(formattedValue)") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/StackViewController.swift b/BenchmarkTests/UIKitCatalog/StackViewController.swift new file mode 100755 index 0000000000..b8859f258b --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/StackViewController.swift @@ -0,0 +1,98 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates different options for manipulating `UIStackView` content. +*/ + +import UIKit + +class StackViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var furtherDetailStackView: UIStackView! + @IBOutlet var plusButton: UIButton! + @IBOutlet var addRemoveExampleStackView: UIStackView! + @IBOutlet var addArrangedViewButton: UIButton! + @IBOutlet var removeArrangedViewButton: UIButton! + + let maximumArrangedSubviewCount = 3 + + // MARK: - View Life Cycle + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + furtherDetailStackView.isHidden = true + plusButton.isHidden = false + updateAddRemoveButtons() + } + + // MARK: - Actions + + @IBAction func showFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let showDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeIn, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = false + self?.plusButton.isHidden = true + }) + showDetailAnimator.startAnimation() + } + + @IBAction func hideFurtherDetail(_: AnyObject) { + // Animate the changes by performing them in a `UIViewPropertyAnimator` animation block. + let hideDetailAnimator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut, animations: { [weak self] in + // Reveal the further details stack view and hide the plus button. + self?.furtherDetailStackView.isHidden = true + self?.plusButton.isHidden = false + }) + hideDetailAnimator.startAnimation() + } + + @IBAction func addArrangedSubviewToStack(_: AnyObject) { + // Create a simple, fixed-size, square view to add to the stack view. + let newViewSize = CGSize(width: 38, height: 38) + let newView = UIView(frame: CGRect(origin: CGPoint.zero, size: newViewSize)) + newView.backgroundColor = randomColor() + newView.widthAnchor.constraint(equalToConstant: newViewSize.width).isActive = true + newView.heightAnchor.constraint(equalToConstant: newViewSize.height).isActive = true + + // Adding an arranged subview automatically adds it as a child of the stack view. + addRemoveExampleStackView.addArrangedSubview(newView) + + updateAddRemoveButtons() + } + + @IBAction func removeArrangedSubviewFromStack(_: AnyObject) { + // Make sure there is an arranged view to remove. + guard let viewToRemove = addRemoveExampleStackView.arrangedSubviews.last else { return } + + addRemoveExampleStackView.removeArrangedSubview(viewToRemove) + + /** Calling `removeArrangedSubview` does not remove the provided view from + the stack view's `subviews` array. Since we no longer want the view + we removed to appear, we have to explicitly remove it from its superview. + */ + viewToRemove.removeFromSuperview() + + updateAddRemoveButtons() + } + + // MARK: - Convenience + + func updateAddRemoveButtons() { + let arrangedSubviewCount = addRemoveExampleStackView.arrangedSubviews.count + + addArrangedViewButton.isEnabled = arrangedSubviewCount < maximumArrangedSubviewCount + removeArrangedViewButton.isEnabled = arrangedSubviewCount > 0 + } + + func randomColor() -> UIColor { + let red = CGFloat(arc4random_uniform(255)) / 255.0 + let green = CGFloat(arc4random_uniform(255)) / 255.0 + let blue = CGFloat(arc4random_uniform(255)) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: 1.0) + } +} diff --git a/BenchmarkTests/UIKitCatalog/StepperViewController.swift b/BenchmarkTests/UIKitCatalog/StepperViewController.swift new file mode 100755 index 0000000000..216fc7e0cf --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/StepperViewController.swift @@ -0,0 +1,97 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIStepper`. +*/ + +import UIKit + +class StepperViewController: BaseTableViewController { + + // Cell identifier for each stepper table view cell. + enum StepperKind: String, CaseIterable { + case defaultStepper + case tintedStepper + case customStepper + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.defaultStepper.rawValue, + configHandler: configureDefaultStepper), + CaseElement(title: NSLocalizedString("TintedStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.tintedStepper.rawValue, + configHandler: configureTintedStepper), + CaseElement(title: NSLocalizedString("CustomStepperTitle", bundle: .module, comment: ""), + cellID: StepperKind.customStepper.rawValue, + configHandler: configureCustomStepper) + ]) + } + + // MARK: - Configuration + + func configureDefaultStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 10, initial value 0, increment/decrement factor of 1. + stepper.value = 0 + stepper.minimumValue = 0 + stepper.maximumValue = 10 + stepper.stepValue = 1 + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureTintedStepper(stepper: UIStepper) { + // Setup the stepper range 0 to 20, initial value 20, increment/decrement factor of 1. + stepper.value = 20 + stepper.minimumValue = 0 + stepper.maximumValue = 20 + stepper.stepValue = 1 + + stepper.tintColor = UIColor(named: "tinted_stepper_control", in: .module, compatibleWith: nil)! + stepper.setDecrementImage(stepper.decrementImage(for: .normal), for: .normal) + stepper.setIncrementImage(stepper.incrementImage(for: .normal), for: .normal) + + stepper.addTarget(self, + action: #selector(StepperViewController.stepperValueDidChange(_:)), + for: .valueChanged) + } + + func configureCustomStepper(stepper: UIStepper) { + // Set the background image. + let stepperBackgroundImage = UIImage(named: "background", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperBackgroundImage, for: .normal) + + let stepperHighlightedBackgroundImage = UIImage(named: "background_highlighted", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperHighlightedBackgroundImage, for: .highlighted) + + let stepperDisabledBackgroundImage = UIImage(named: "background_disabled", in: .module, compatibleWith: nil) + stepper.setBackgroundImage(stepperDisabledBackgroundImage, for: .disabled) + + // Set the image which will be painted in between the two stepper segments. It depends on the states of both segments. + let stepperSegmentDividerImage = UIImage(named: "stepper_and_segment_divider", in: .module, compatibleWith: nil) + stepper.setDividerImage(stepperSegmentDividerImage, forLeftSegmentState: .normal, rightSegmentState: .normal) + + // Set the image for the + button. + let stepperIncrementImage = UIImage(systemName: "plus") + stepper.setIncrementImage(stepperIncrementImage, for: .normal) + + // Set the image for the - button. + let stepperDecrementImage = UIImage(systemName: "minus") + stepper.setDecrementImage(stepperDecrementImage, for: .normal) + + stepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) + } + + // MARK: - Actions + + @objc + func stepperValueDidChange(_ stepper: UIStepper) { + Swift.debugPrint("A stepper changed its value: \(stepper.value).") + } +} diff --git a/BenchmarkTests/UIKitCatalog/SwitchViewController.swift b/BenchmarkTests/UIKitCatalog/SwitchViewController.swift new file mode 100755 index 0000000000..fddd6494f5 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SwitchViewController.swift @@ -0,0 +1,91 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UISwitch`. +*/ + +import UIKit + +class SwitchViewController: BaseTableViewController { + + // Cell identifier for each switch table view cell. + enum SwitchKind: String, CaseIterable { + case defaultSwitch + case checkBoxSwitch + case tintedSwitch + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.defaultSwitch.rawValue, + configHandler: configureDefaultSwitch) + ]) + + // Checkbox switch is available only when running on macOS. + if navigationController!.traitCollection.userInterfaceIdiom == .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("CheckboxSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.checkBoxSwitch.rawValue, + configHandler: configureCheckboxSwitch) + ]) + } + + // Tinted switch is available only when running on iOS. + if navigationController!.traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("TintedSwitchTitle", bundle: .module, comment: ""), + cellID: SwitchKind.tintedSwitch.rawValue, + configHandler: configureTintedSwitch) + ]) + } + } + + // MARK: - Configuration + + func configureDefaultSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + switchControl.preferredStyle = .sliding + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + func configureCheckboxSwitch(_ switchControl: UISwitch) { + switchControl.setOn(true, animated: false) + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + + // On the Mac, make sure this control take on the apperance of a checkbox with a title. + if traitCollection.userInterfaceIdiom == .mac { + switchControl.preferredStyle = .checkbox + + // Title on a UISwitch is only supported when running Catalyst apps in the Mac Idiom. + switchControl.title = NSLocalizedString("SwitchTitle", bundle: .module, comment: "") + } + } + + func configureTintedSwitch(_ switchControl: UISwitch) { + switchControl.tintColor = UIColor.systemBlue + switchControl.onTintColor = UIColor.systemGreen + switchControl.thumbTintColor = UIColor.systemPurple + + switchControl.addTarget(self, + action: #selector(SwitchViewController.switchValueDidChange(_:)), + for: .valueChanged) + } + + // MARK: - Actions + + @objc + func switchValueDidChange(_ aSwitch: UISwitch) { + Swift.debugPrint("A switch changed its value: \(aSwitch.isOn).") + } + +} diff --git a/BenchmarkTests/UIKitCatalog/SymbolViewController.swift b/BenchmarkTests/UIKitCatalog/SymbolViewController.swift new file mode 100755 index 0000000000..70c4ea030c --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/SymbolViewController.swift @@ -0,0 +1,106 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use SF Symbols. +*/ + +import UIKit + +class SymbolViewController: BaseTableViewController { + + // Cell identifier for each SF Symbol table view cell. + enum SymbolKind: String, CaseIterable { + case plainSymbol + case tintedSymbol + case largeSizeSymbol + case hierarchicalColorSymbol + case paletteColorsSymbol + case preferringMultiColorSymbol + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("PlainSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.plainSymbol.rawValue, + configHandler: configurePlainSymbol), + CaseElement(title: NSLocalizedString("TintedSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.tintedSymbol.rawValue, + configHandler: configureTintedSymbol), + CaseElement(title: NSLocalizedString("LargeSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.largeSizeSymbol.rawValue, + configHandler: configureLargeSizeSymbol) + ]) + + if #available(iOS 15, *) { + // These type SF Sybols, and variants are available on iOS 15, Mac Catalyst 15 or later. + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("HierarchicalSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.hierarchicalColorSymbol.rawValue, + configHandler: configureHierarchicalSymbol), + CaseElement(title: NSLocalizedString("PaletteSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.paletteColorsSymbol.rawValue, + configHandler: configurePaletteColorsSymbol), + CaseElement(title: NSLocalizedString("PreferringMultiColorSymbolTitle", bundle: .module, comment: ""), + cellID: SymbolKind.preferringMultiColorSymbol.rawValue, + configHandler: configurePreferringMultiColorSymbol) + ]) + } + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let cellTest = testCells[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: cellTest.cellID) + return cell!.contentView.bounds.size.height + } + + // MARK: - Configuration + + func configurePlainSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + } + + func configureTintedSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + imageView.tintColor = .systemPurple + } + + func configureLargeSizeSymbol(_ imageView: UIImageView) { + let image = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = image + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 32, weight: .heavy, scale: .large) + imageView.preferredSymbolConfiguration = symbolConfig + } + + @available(iOS 15.0, *) + func configureHierarchicalSymbol(_ imageView: UIImageView) { + let imageConfig = UIImage.SymbolConfiguration(hierarchicalColor: UIColor.systemRed) + let hierarchicalSymbol = UIImage(systemName: "cloud.sun.rain.fill") + imageView.image = hierarchicalSymbol + imageView.preferredSymbolConfiguration = imageConfig + } + + @available(iOS 15.0, *) + func configurePaletteColorsSymbol(_ imageView: UIImageView) { + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemRed, UIColor.systemOrange, UIColor.systemYellow]) + let palleteSymbol = UIImage(systemName: "battery.100.bolt") + imageView.image = palleteSymbol + imageView.backgroundColor = UIColor.darkText + imageView.preferredSymbolConfiguration = palleteSymbolConfig + } + + @available(iOS 15.0, *) + func configurePreferringMultiColorSymbol(_ imageView: UIImageView) { + let preferredSymbolConfig = UIImage.SymbolConfiguration.preferringMulticolor() + let preferredSymbol = UIImage(systemName: "circle.hexagongrid.fill") + imageView.image = preferredSymbol + imageView.preferredSymbolConfiguration = preferredSymbolConfig + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift b/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift new file mode 100755 index 0000000000..23d2a4153d --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TextFieldViewController.swift @@ -0,0 +1,181 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UITextField`. +*/ + +import UIKit + +class TextFieldViewController: BaseTableViewController { + + // Cell identifier for each text field table view cell. + enum TextFieldKind: String, CaseIterable { + case textField + case tintedTextField + case secureTextField + case specificKeyboardTextField + case customTextField + case searchTextField + } + + override func viewDidLoad() { + super.viewDidLoad() + + testCells.append(contentsOf: [ + CaseElement(title: NSLocalizedString("DefaultTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.textField.rawValue, + configHandler: configureTextField), + CaseElement(title: NSLocalizedString("TintedTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.tintedTextField.rawValue, + configHandler: configureTintedTextField), + CaseElement(title: NSLocalizedString("SecuretTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.secureTextField.rawValue, + configHandler: configureSecureTextField), + CaseElement(title: NSLocalizedString("SearchTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.searchTextField.rawValue, + configHandler: configureSearchTextField) + ]) + + if traitCollection.userInterfaceIdiom != .mac { + testCells.append(contentsOf: [ + // Show text field with specific kind of keyboard for iOS only. + CaseElement(title: NSLocalizedString("SpecificKeyboardTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.specificKeyboardTextField.rawValue, + configHandler: configureSpecificKeyboardTextField), + + // Show text field with custom background for iOS only. + CaseElement(title: NSLocalizedString("CustomTextFieldTitle", bundle: .module, comment: ""), + cellID: TextFieldKind.customTextField.rawValue, + configHandler: configureCustomTextField) + ]) + } + } + + // MARK: - Configuration + + func configureTextField(_ textField: UITextField) { + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.autocorrectionType = .yes + textField.returnKeyType = .done + textField.clearButtonMode = .whileEditing + } + + func configureTintedTextField(_ textField: UITextField) { + textField.tintColor = UIColor.systemBlue + textField.textColor = UIColor.systemGreen + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .never + } + + func configureSecureTextField(_ textField: UITextField) { + textField.isSecureTextEntry = true + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + textField.clearButtonMode = .always + } + + func configureSearchTextField(_ textField: UITextField) { + if let searchField = textField as? UISearchTextField { + searchField.placeholder = NSLocalizedString("Enter search text", bundle: .module, comment: "") + searchField.returnKeyType = .done + searchField.clearButtonMode = .always + searchField.allowsDeletingTokens = true + + // Setup the left view as a symbol image view. + let searchIcon = UIImageView(image: UIImage(systemName: "magnifyingglass")) + searchIcon.tintColor = UIColor.systemGray + searchField.leftView = searchIcon + searchField.leftViewMode = .always + + let secondToken = UISearchToken(icon: UIImage(systemName: "staroflife"), text: "Token 2") + searchField.insertToken(secondToken, at: 0) + + let firstToken = UISearchToken(icon: UIImage(systemName: "staroflife.fill"), text: "Token 1") + searchField.insertToken(firstToken, at: 0) + } + } + + /** There are many different types of keyboards that you may choose to use. + The different types of keyboards are defined in the `UITextInputTraits` interface. + This example shows how to display a keyboard to help enter email addresses. + */ + func configureSpecificKeyboardTextField(_ textField: UITextField) { + textField.keyboardType = .emailAddress + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.returnKeyType = .done + } + + func configureCustomTextField(_ textField: UITextField) { + // Text fields with custom image backgrounds must have no border. + textField.borderStyle = .none + + textField.background = UIImage(named: "text_field_background", in: .module, compatibleWith: nil) + + // Create a purple button to be used as the right view of the custom text field. + let purpleImage = UIImage(named: "text_field_purple_right_view", in: .module, compatibleWith: nil)! + let purpleImageButton = UIButton(type: .custom) + purpleImageButton.bounds = CGRect(x: 0, y: 0, width: purpleImage.size.width, height: purpleImage.size.height) + purpleImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) + purpleImageButton.setImage(purpleImage, for: .normal) + purpleImageButton.addTarget(self, action: #selector(TextFieldViewController.customTextFieldPurpleButtonClicked), for: .touchUpInside) + textField.rightView = purpleImageButton + textField.rightViewMode = .always + + textField.placeholder = NSLocalizedString("Placeholder text", bundle: .module, comment: "") + textField.autocorrectionType = .no + textField.clearButtonMode = .never + textField.returnKeyType = .done + } + + // MARK: - Actions + + @objc + func customTextFieldPurpleButtonClicked() { + Swift.debugPrint("The custom text field's purple right view button was clicked.") + } + +} + +// MARK: - UITextFieldDelegate + +extension TextFieldViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + func textFieldDidChangeSelection(_ textField: UITextField) { + // User changed the text selection. + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Return false to not change text. + return true + } +} + +// Custom text field for controlling input text placement. +class CustomTextField: UITextField { + let leftMarginPadding: CGFloat = 12 + let rightMarginPadding: CGFloat = 36 + + override func textRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + var rect = bounds + rect.origin.x += leftMarginPadding + rect.size.width -= rightMarginPadding + return rect + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TextViewController.swift b/BenchmarkTests/UIKitCatalog/TextViewController.swift new file mode 100755 index 0000000000..b1d71f03ef --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TextViewController.swift @@ -0,0 +1,237 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UITextView`. +*/ + +import UIKit + +class TextViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var textView: UITextView! + + /// Used to adjust the text view's height when the keyboard hides and shows. + @IBOutlet weak var textViewBottomLayoutGuideConstraint: NSLayoutConstraint! + + lazy var font = UIFont( + descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body), + size: 0) + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + configureTextView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Listen for changes to keyboard visibility so that we can adjust the text view's height accordingly. + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil) + + notificationCenter.addObserver(self, + selector: #selector(TextViewController.handleKeyboardNotification(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + notificationCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + + // MARK: - Keyboard Event Notifications + + @objc + func handleKeyboardNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo else { return } + + // Get the animation duration. + var animationDuration: TimeInterval = 0 + if let value = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber { + animationDuration = value.doubleValue + } + + // Convert the keyboard frame from screen to view coordinates. + var keyboardScreenBeginFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue) { + keyboardScreenBeginFrame = value.cgRectValue + } + + var keyboardScreenEndFrame = CGRect() + if let value = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue) { + keyboardScreenEndFrame = value.cgRectValue + } + + let keyboardViewBeginFrame = view.convert(keyboardScreenBeginFrame, from: view.window) + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) + + let originDelta = keyboardViewEndFrame.origin.y - keyboardViewBeginFrame.origin.y + + // The text view should be adjusted, update the constant for this constraint. + textViewBottomLayoutGuideConstraint.constant -= originDelta + + // Inform the view that its autolayout constraints have changed and the layout should be updated. + view.setNeedsUpdateConstraints() + + // Animate updating the view's layout by calling layoutIfNeeded inside a `UIViewPropertyAnimator` animation block. + let textViewAnimator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeIn, animations: { [weak self] in + self?.view.layoutIfNeeded() + }) + textViewAnimator.startAnimation() + + // Scroll to the selected text once the keyboard frame changes. + let selectedRange = textView.selectedRange + textView.scrollRangeToVisible(selectedRange) + } + + // MARK: - Configuration + + func reflowTextAttributes() { + var entireTextColor = UIColor.black + + // The text should be white in dark mode. + if self.view.traitCollection.userInterfaceStyle == .dark { + entireTextColor = UIColor.white + } + let entireAttributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + let entireRange = NSRange(location: 0, length: entireAttributedText.length) + entireAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: entireTextColor, range: entireRange) + textView.attributedText = entireAttributedText + + /** Modify some of the attributes of the attributed string. + You can modify these attributes yourself to get a better feel for what they do. + Note that the initial text is visible in the storyboard. + */ + let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + + /** Use NSString so the result of rangeOfString is an NSRange, not Range. + This will then be the correct type to then pass to the addAttribute method of NSMutableAttributedString. + */ + let text = textView.text! as NSString + + // Find the range of each element to modify. + let boldRange = text.range(of: NSLocalizedString("bold", bundle: .module, comment: "")) + let highlightedRange = text.range(of: NSLocalizedString("highlighted", bundle: .module, comment: "")) + let underlinedRange = text.range(of: NSLocalizedString("underlined", bundle: .module, comment: "")) + let tintedRange = text.range(of: NSLocalizedString("tinted", bundle: .module, comment: "")) + + // Add bold attribute. Take the current font descriptor and create a new font descriptor with an additional bold trait. + let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) + let boldFont = UIFont(descriptor: boldFontDescriptor!, size: 0) + attributedText.addAttribute(NSAttributedString.Key.font, value: boldFont, range: boldRange) + + // Add highlight attribute. + attributedText.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.systemGreen, range: highlightedRange) + + // Add underline attribute. + attributedText.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: underlinedRange) + + // Add tint color. + attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemBlue, range: tintedRange) + + textView.attributedText = attributedText + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + // With the background change, we need to re-apply the text attributes. + reflowTextAttributes() + } + + func symbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + if let symbolImage = UIImage(systemName: name)?.withRenderingMode(.alwaysTemplate) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + @available(iOS 15.0, *) + func multiColorSymbolAttributedString(name: String) -> NSAttributedString { + let symbolAttachment = NSTextAttachment() + let palleteSymbolConfig = UIImage.SymbolConfiguration(paletteColors: [UIColor.systemOrange, UIColor.systemRed]) + if let symbolImage = UIImage(systemName: name)?.withConfiguration(palleteSymbolConfig) { + symbolAttachment.image = symbolImage + } + return NSAttributedString(attachment: symbolAttachment) + } + + func configureTextView() { + textView.font = font + textView.backgroundColor = UIColor(named: "text_view_background", in: .module, compatibleWith: nil) + + textView.isScrollEnabled = true + + // Apply different attributes to the text (bold, tinted, underline, etc.). + reflowTextAttributes() + + // Insert symbols as image attachments. + let text = textView.text! as NSString + let attributedText = NSMutableAttributedString(attributedString: textView.attributedText!) + let symbolsSearchRange = text.range(of: NSLocalizedString("symbols", bundle: .module, comment: "")) + var insertPoint = symbolsSearchRange.location + symbolsSearchRange.length + attributedText.insert(symbolAttributedString(name: "heart"), at: insertPoint) + insertPoint += 1 + attributedText.insert(symbolAttributedString(name: "heart.fill"), at: insertPoint) + insertPoint += 1 + attributedText.insert(symbolAttributedString(name: "heart.slash"), at: insertPoint) + + // Multi-color SF Symbols only in iOS 15 or later. + if #available(iOS 15, *) { + insertPoint += 1 + attributedText.insert(multiColorSymbolAttributedString(name: "arrow.up.heart.fill"), at: insertPoint) + } + + // Add the image as an attachment. + if let image = UIImage(named: "text_view_attachment", in: .module, compatibleWith: nil) { + let textAttachment = NSTextAttachment() + textAttachment.image = image + textAttachment.bounds = CGRect(origin: CGPoint.zero, size: image.size) + let textAttachmentString = NSAttributedString(attachment: textAttachment) + attributedText.append(textAttachmentString) + textView.attributedText = attributedText + } + + /** When turned on, this changes the rendering scale of the text to match the standard text scaling + and preserves the original font point sizes when the contents of the text view are copied to the pasteboard. + Apps that show a lot of text content, such as a text viewer or editor, should turn this on and use the standard text scaling. + */ + textView.usesStandardTextScaling = true + } + + // MARK: - Actions + + @objc + func doneBarButtonItemClicked() { + // Dismiss the keyboard by removing it as the first responder. + textView.resignFirstResponder() + + navigationItem.setRightBarButton(nil, animated: true) + } +} + +// MARK: - UITextViewDelegate + +extension TextViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + // Provide a "Done" button for the user to end text editing. + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, + target: self, + action: #selector(TextViewController.doneBarButtonItemClicked)) + + navigationItem.setRightBarButton(doneBarButtonItem, animated: true) + } + +} diff --git a/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift b/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift new file mode 100755 index 0000000000..430ac755ee --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/TintedToolbarViewController.swift @@ -0,0 +1,76 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to customize a `UIToolbar`. +*/ + +import UIKit + +class TintedToolbarViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var toolbar: UIToolbar! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // See the `UIBarStyle` enum for more styles, including `.Default`. + toolbar.barStyle = .black + toolbar.isTranslucent = false + + toolbar.tintColor = UIColor.systemGreen + toolbar.backgroundColor = UIColor.systemBlue + + let toolbarButtonItems = [ + refreshBarButtonItem, + flexibleSpaceBarButtonItem, + actionBarButtonItem + ] + toolbar.setItems(toolbarButtonItems, animated: true) + } + + // MARK: - `UIBarButtonItem` Creation and Configuration + + var refreshBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .refresh, + target: self, + action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) + } + + var flexibleSpaceBarButtonItem: UIBarButtonItem { + // Note that there's no target/action since this represents empty space. + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, + target: nil, + action: nil) + } + + var actionBarButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .action, + target: self, + action: #selector(TintedToolbarViewController.actionBarButtonItemClicked(_:))) + } + + // MARK: - Actions + + @objc + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + Swift.debugPrint("A bar button item on the tinted toolbar was clicked: \(barButtonItem).") + } + + @objc + func actionBarButtonItemClicked(_ barButtonItem: UIBarButtonItem) { + if let image = UIImage(named: "Flowers_1", in: .module, compatibleWith: nil) { + let activityItems = ["Shared piece of text", image] as [Any] + + let activityViewController = + UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + + activityViewController.popoverPresentationController?.barButtonItem = barButtonItem + present(activityViewController, animated: true, completion: nil) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements b/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements new file mode 100755 index 0000000000..ee95ab7e58 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/UIKitCatalog.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift b/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift new file mode 100755 index 0000000000..521604f4e0 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/VisualEffectViewController.swift @@ -0,0 +1,68 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `UIVisualEffectView`. +*/ + +import UIKit + +class VisualEffectViewController: UIViewController { + // MARK: - Properties + + @IBOutlet var imageView: UIImageView! + + private var visualEffect: UIVisualEffectView = { + let vev = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + vev.translatesAutoresizingMaskIntoConstraints = false + return vev + }() + + private var textView: UITextView = { + let textView = UITextView(frame: CGRect()) + textView.font = UIFont.systemFont(ofSize: 14) + textView.text = NSLocalizedString("VisualEffectTextContent", bundle: .module, comment: "") + + textView.translatesAutoresizingMaskIntoConstraints = false + textView.backgroundColor = UIColor.clear + if let fontDescriptor = UIFontDescriptor + .preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body) + .withSymbolicTraits(UIFontDescriptor.SymbolicTraits.traitLooseLeading) { + let looseLeadingFont = UIFont(descriptor: fontDescriptor, size: 0) + textView.font = looseLeadingFont + } + return textView + }() + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Add the visual effect view in the same area covering the image view. + view.addSubview(visualEffect) + NSLayoutConstraint.activate([ + visualEffect.topAnchor.constraint(equalTo: imageView.topAnchor), + visualEffect.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), + visualEffect.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + visualEffect.bottomAnchor.constraint(equalTo: imageView.bottomAnchor) + ]) + + // Add a text view as a subview to the visual effect view. + visualEffect.contentView.addSubview(textView) + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.topAnchor), + textView.leadingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: visualEffect.safeAreaLayoutGuide.bottomAnchor) + ]) + + if #available(iOS 15, *) { + // Use UIToolTipInteraction which is available on iOS 15 or later, add it to the image view. + let toolTipString = NSLocalizedString("VisualEffectToolTipTitle", bundle: .module, comment: "") + let interaction = UIToolTipInteraction(defaultToolTip: toolTipString) + imageView.addInteraction(interaction) + } + } + +} diff --git a/BenchmarkTests/UIKitCatalog/WebViewController.swift b/BenchmarkTests/UIKitCatalog/WebViewController.swift new file mode 100755 index 0000000000..2b462a81f6 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/WebViewController.swift @@ -0,0 +1,59 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +A view controller that demonstrates how to use `WKWebView`. +*/ + +import UIKit +import WebKit + +/** NOTE: + If your app customizes, interacts with, or controls the display of web content, use the WKWebView class. + If you want to view a website from anywhere on the Internet, use the SFSafariViewController class. + */ + +class WebViewController: UIViewController { + // MARK: - Properties + + @IBOutlet weak var webView: WKWebView! + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // So we can capture failures in "didFailProvisionalNavigation". + webView.navigationDelegate = self + loadAddressURL() + } + + // MARK: - Loading + + func loadAddressURL() { + // Set the content to local html in our app bundle. + if let url = Bundle.module.url(forResource: "content", withExtension: "html") { + webView.loadFileURL(url, allowingReadAccessTo: url) + } + } + +} + +// MARK: - WKNavigationDelegate + +extension WebViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + let webKitError = error as NSError + if webKitError.code == NSURLErrorNotConnectedToInternet { + // Report the error inside the web view. + let localizedErrorMessage = NSLocalizedString("An error occurred:", bundle: .module, comment: "") + + let message = "\(localizedErrorMessage) \(error.localizedDescription)" + let errorHTML = + "
\(message)
" + + webView.loadHTMLString(errorHTML, baseURL: nil) + } + } + +} diff --git a/BenchmarkTests/exportOptions.plist b/BenchmarkTests/exportOptions.plist new file mode 100644 index 0000000000..00cd98869b --- /dev/null +++ b/BenchmarkTests/exportOptions.plist @@ -0,0 +1,19 @@ + + + + + distributionBundleIdentifier + com.datadoghq.benchmarks.Runner + method + development + provisioningProfiles + + com.datadoghq.benchmarks.Runner + Datadog Benchmark Runner + + signingCertificate + Apple Development: Robot Bitrise (9HKDHCMCGH) + teamID + JKFCB4CN7C + + diff --git a/BenchmarkTests/xcconfigs/Runner.xcconfig b/BenchmarkTests/xcconfigs/Runner.xcconfig new file mode 100644 index 0000000000..251d60c004 --- /dev/null +++ b/BenchmarkTests/xcconfigs/Runner.xcconfig @@ -0,0 +1,9 @@ +CLIENT_TOKEN = // the Client Token on Mobile Integration Org +RUM_APPLICATION_ID = // the RUM Application ID on Mobile Integration Org +API_KEY = // the API Key on Mobile Integration Org + +DD_ENV[config=*] = benchmarks +DD_ENV[config=Debug] = development +DD_SITE = us1 + +#include? "Benchmarks.local.xcconfig" diff --git a/BenchmarkTests/xcconfigs/Synthetics.xcconfig b/BenchmarkTests/xcconfigs/Synthetics.xcconfig new file mode 100644 index 0000000000..b7e14c3c51 --- /dev/null +++ b/BenchmarkTests/xcconfigs/Synthetics.xcconfig @@ -0,0 +1,6 @@ +#include "Runner.xcconfig" + +CODE_SIGN_STYLE = Manual +CODE_SIGN_IDENTITY = Apple Development: Robot Bitrise (9HKDHCMCGH) +DEVELOPMENT_TEAM = JKFCB4CN7C +PROVISIONING_PROFILE_SPECIFIER = Datadog Benchmark Runner diff --git a/CHANGELOG.md b/CHANGELOG.md index 283ebe244d..43672d77f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Unreleased +- [IMPROVEMENT] Add Datadog Configuration `backgroundTasksEnabled` ObjC API. See [#2148][] +- [FIX] Prevent Session Replay to create two full snapshots in a row. See [#2154][] + +# 2.21.0 / 11-12-2024 + +- [FIX] Fix sporadic file overwrite during consent change, ensuring event data integrity. See [#2113][] +- [FIX] Fix trace inconsistency when using `URLSessionInterceptor` or Alamofire extension. See [#2114][] +- [IMPROVEMENT] Add Session Replay `startRecordingImmediately` ObjC API. See [#2120][] +- [IMPROVEMENT] Expose Crash Reporter Plugin Publicly. See [#2116][] (Thanks [@naftaly][]) [#2126][] + +# 2.20.0 / 14-11-2024 + +- [FIX] Fix race condition during consent change, preventing loss of events recorded on the current thread. See [#2063][] +- [IMPROVEMENT] Support mutation of events' attributes. See [#2099][] +- [IMPROVEMENT] Add 'os' and 'device' info to Span events. See [#2104][] +- [FIX] Fix bug in SR that was enforcing full snapshot more often than needed. See [#2092][] + +# 2.19.0 / 28-10-2024 + +- [FEATURE] Add Privacy Overrides in Session Replay. See [#2088][] +- [IMPROVEMENT] Add ObjC API for the internal logging/telemetry. See [#2073][] +- [IMPROVEMENT] Support `clipsToBounds` in Session Replay. See [#2083][] + +# 2.18.0 / 25-09-2024 +- [IMPROVEMENT] Add overwrite required (breaking) param to addViewLoadingTime & usage telemetry. See [#2040][] +- [FEATURE] Prevent "show password" features from revealing sensitive texts in Session Replay. See [#2050][] +- [FEATURE] Add Fine-Grained Masking configuration options to Session Replay. See [#2043][] + +# 2.17.0 / 11-09-2024 + +- [FEATURE] Add support for view loading experimental API (addViewLoadingTime). See [#2026][] +- [IMPROVEMENT] Drop support for deprecated cocoapod specs. See [#1998][] +- [FIX] Propagate global Tracer tags to OpenTelemetry span attributes. See [#2000][] +- [FEATURE] Add Logs event mapper to ObjC API. See [#2008][] +- [IMPROVEMENT] Send retry information with network requests (eg. retry_count, last_failure_status and idempotency key). See [#1991][] +- [IMPROVEMENT] Enable app launch time on mac, macCatalyst and visionOS. See [#1888][] (Thanks [@Hengyu][]) +- [FIX] Ignore network reachability on watchOS . See [#2005][] (Thanks [@jfiser-paylocity][]) +- [FEATURE] Add Start / Stop API to Session Replay (start/stopRecording). See [#1986][] + +# 2.16.0 / 20-08-2024 + +- [IMPROVEMENT] Deprecate Alamofire extension pod. See [#1966][] +- [FIX] Refresh rate vital for variable refresh rate displays when over performing. See [#1973][] +- [FIX] Alamofire extension types are deprecated now. See [#1988][] + +# 2.14.2 / 26-07-2024 + +- [FIX] Fix CPU spikes when Watchdog Terminations tracking is enabled. See #1968 +- [FIX] Fix CPU spike when recording UITabBar using SessionReplay. See #1967 + +# 2.15.0 / 25-07-2024 + +- [FEATURE] Enable DatadogCore, DatadogLogs and DatadogTrace to compile on watchOS platform. See [#1918][] (Thanks [@jfiser-paylocity][]) [#1946][] +- [IMPROVEMENT] Ability to clear feature data storage using `clearAllData` API. See [#1940][] +- [IMPROVEMENT] Send memory warning as RUM error. See [#1955][] +- [IMPROVEMENT] Decorate network span kind as `client`. See [#1963][] +- [FIX] Fix CPU spikes when Watchdog Terminations tracking is enabled. See [#1968][] +- [FIX] Fix CPU spike when recording UITabBar using SessionReplay. See [#1967][] + +# 2.14.1 / 09-07-2024 + +- [FIX] Objc attributes interop for KMP. See [#1947][] +- [FIX] Inject backtrace reporter into Logs feature. See [#1948][] + +# 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][] +- [IMPROVEMENT] Use otel-swift fork that only has APIs. See [#1930][] + +# 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 +762,55 @@ 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 +[#1930]: https://github.com/DataDog/dd-sdk-ios/pull/1930 +[#1918]: https://github.com/DataDog/dd-sdk-ios/pull/1918 +[#1946]: https://github.com/DataDog/dd-sdk-ios/pull/1946 +[#1934]: https://github.com/DataDog/dd-sdk-ios/pull/1934 +[#1938]: https://github.com/DataDog/dd-sdk-ios/pull/1938 +[#1947]: https://github.com/DataDog/dd-sdk-ios/pull/1947 +[#1948]: https://github.com/DataDog/dd-sdk-ios/pull/1948 +[#1940]: https://github.com/DataDog/dd-sdk-ios/pull/1940 +[#1955]: https://github.com/DataDog/dd-sdk-ios/pull/1955 +[#1963]: https://github.com/DataDog/dd-sdk-ios/pull/1963 +[#1968]: https://github.com/DataDog/dd-sdk-ios/pull/1968 +[#1967]: https://github.com/DataDog/dd-sdk-ios/pull/1967 +[#1973]: https://github.com/DataDog/dd-sdk-ios/pull/1973 +[#1988]: https://github.com/DataDog/dd-sdk-ios/pull/1988 +[#2000]: https://github.com/DataDog/dd-sdk-ios/pull/2000 +[#1991]: https://github.com/DataDog/dd-sdk-ios/pull/1991 +[#1986]: https://github.com/DataDog/dd-sdk-ios/pull/1986 +[#1888]: https://github.com/DataDog/dd-sdk-ios/pull/1888 +[#2008]: https://github.com/DataDog/dd-sdk-ios/pull/2008 +[#2005]: https://github.com/DataDog/dd-sdk-ios/pull/2005 +[#1998]: https://github.com/DataDog/dd-sdk-ios/pull/1998 +[#1966]: https://github.com/DataDog/dd-sdk-ios/pull/1966 +[#2026]: https://github.com/DataDog/dd-sdk-ios/pull/2026 +[#2043]: https://github.com/DataDog/dd-sdk-ios/pull/2043 +[#2040]: https://github.com/DataDog/dd-sdk-ios/pull/2040 +[#2050]: https://github.com/DataDog/dd-sdk-ios/pull/2050 +[#2073]: https://github.com/DataDog/dd-sdk-ios/pull/2073 +[#2088]: https://github.com/DataDog/dd-sdk-ios/pull/2088 +[#2083]: https://github.com/DataDog/dd-sdk-ios/pull/2083 +[#2104]: https://github.com/DataDog/dd-sdk-ios/pull/2104 +[#2099]: https://github.com/DataDog/dd-sdk-ios/pull/2099 +[#2063]: https://github.com/DataDog/dd-sdk-ios/pull/2063 +[#2092]: https://github.com/DataDog/dd-sdk-ios/pull/2092 +[#2113]: https://github.com/DataDog/dd-sdk-ios/pull/2113 +[#2114]: https://github.com/DataDog/dd-sdk-ios/pull/2114 +[#2116]: https://github.com/DataDog/dd-sdk-ios/pull/2116 +[#2120]: https://github.com/DataDog/dd-sdk-ios/pull/2120 +[#2126]: https://github.com/DataDog/dd-sdk-ios/pull/2126 +[#2148]: https://github.com/DataDog/dd-sdk-ios/pull/2148 +[#2154]: https://github.com/DataDog/dd-sdk-ios/pull/2154 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu @@ -714,3 +841,6 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [@cltnschlosser]: https://github.com/cltnschlosser [@alexfanatics]: https://github.com/alexfanatics [@changm4n]: https://github.com/changm4n +[@jfiser-paylocity]: https://github.com/jfiser-paylocity +[@Hengyu]: https://github.com/Hengyu +[@naftaly]: https://github.com/naftaly 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..8c1db17e4b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -7,12 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 116F84062CFDD06700705755 /* SampleRateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116F84052CFDD06700705755 /* SampleRateTests.swift */; }; + 116F84072CFDD06700705755 /* SampleRateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116F84052CFDD06700705755 /* SampleRateTests.swift */; }; 1434A4612B7F73110072E3BB /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; 1434A4622B7F73110072E3BB /* OpenTelemetryApi.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1434A4632B7F73170072E3BB /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; 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,13 +44,28 @@ 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 */; }; + 3C4CF9912C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */; }; + 3C4CF9922C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */; }; + 3C4CF9942C47CAE9006DE1C0 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C4CF9952C47CAEA006DE1C0 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C4CF9982C47CC91006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */; }; + 3C4CF9992C47CC92006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */; }; + 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */; }; + 3C4CF99C2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */; }; + 3C5CD8CD2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */; }; + 3C5CD8CE2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.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 */; }; 3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; 3C5D691F2B76825500C4E07E /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; 3C5D69222B76826000C4E07E /* OpenTelemetryApi.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */; }; + 3C62C3612C3E852F00C7E336 /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C62C3602C3E852F00C7E336 /* MultiSelector.swift */; }; 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */; }; 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; @@ -67,6 +88,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 */; }; @@ -91,12 +114,30 @@ 3CCECDB02BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */; }; 3CCECDB22BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; 3CCECDB32BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; + 3CD3A13A2C6C99ED00436A69 /* Data+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */; }; + 3CD3A13B2C6C99ED00436A69 /* Data+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */; }; + 3CD3A13C2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */; }; + 3CD3A13D2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */; }; 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */; }; 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 */; }; @@ -116,10 +157,10 @@ 61054E672A6EE10A00AAA894 /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E112A6EE10A00AAA894 /* Recorder.swift */; }; 61054E682A6EE10A00AAA894 /* PrivacyLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E122A6EE10A00AAA894 /* PrivacyLevel.swift */; }; 61054E692A6EE10A00AAA894 /* UIImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */; }; - 61054E6A2A6EE10A00AAA894 /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E152A6EE10A00AAA894 /* UIKitExtensions.swift */; }; + 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */; }; 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */; }; 61054E6C2A6EE10A00AAA894 /* SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E172A6EE10A00AAA894 /* SystemColors.swift */; }; - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */; }; + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */; }; 61054E6E2A6EE10A00AAA894 /* RecordingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */; }; 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */; }; 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */; }; @@ -145,7 +186,6 @@ 61054E842A6EE10A00AAA894 /* UITabBarRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */; }; 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */; }; 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */; }; - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */; }; 61054E882A6EE10A00AAA894 /* ViewTreeRecordingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */; }; 61054E892A6EE10A00AAA894 /* NodeIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */; }; 61054E8A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */; }; @@ -163,14 +203,13 @@ 61054E992A6EE10A00AAA894 /* WireframesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E512A6EE10A00AAA894 /* WireframesBuilder.swift */; }; 61054E9A2A6EE10A00AAA894 /* NodesFlattener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E532A6EE10A00AAA894 /* NodesFlattener.swift */; }; 61054E9B2A6EE10B00AAA894 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E552A6EE10A00AAA894 /* CGRectExtensions.swift */; }; - 61054E9C2A6EE10B00AAA894 /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E562A6EE10A00AAA894 /* UIImage+Scaling.swift */; }; 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E582A6EE10A00AAA894 /* Queue.swift */; }; 61054E9F2A6EE10B00AAA894 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E592A6EE10A00AAA894 /* Errors.swift */; }; 61054EA02A6EE10B00AAA894 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E5A2A6EE10A00AAA894 /* Colors.swift */; }; 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E5C2A6EE10A00AAA894 /* MainThreadScheduler.swift */; }; 61054EA22A6EE10B00AAA894 /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E5D2A6EE10A00AAA894 /* Scheduler.swift */; }; 61054F952A6EE1BA00AAA894 /* SessionReplayConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F3D2A6EE1B900AAA894 /* SessionReplayConfigurationTests.swift */; }; - 61054F972A6EE1BA00AAA894 /* UIImage+ScalingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F402A6EE1B900AAA894 /* UIImage+ScalingTests.swift */; }; + 61054F972A6EE1BA00AAA894 /* UIImage+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F402A6EE1B900AAA894 /* UIImage+SessionReplayTests.swift */; }; 61054F982A6EE1BA00AAA894 /* CGRectExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F412A6EE1B900AAA894 /* CGRectExtensionsTests.swift */; }; 61054F992A6EE1BA00AAA894 /* ColorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F422A6EE1B900AAA894 /* ColorsTests.swift */; }; 61054F9A2A6EE1BA00AAA894 /* CFType+SafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F432A6EE1B900AAA894 /* CFType+SafetyTests.swift */; }; @@ -187,8 +226,8 @@ 61054FA62A6EE1BA00AAA894 /* SnapshotProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */; }; 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */; }; 61054FA82A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */; }; - 61054FAA2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5D2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift */; }; - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */; }; + 61054FAA2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */; }; + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */; }; 61054FAD2A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */; }; 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */; }; 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */; }; @@ -211,7 +250,7 @@ 61054FC02A6EE1BA00AAA894 /* UITextViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F782A6EE1BA00AAA894 /* UITextViewRecorderTests.swift */; }; 61054FC12A6EE1BA00AAA894 /* ViewTreeRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F792A6EE1BA00AAA894 /* ViewTreeRecorderTests.swift */; }; 61054FC22A6EE1BA00AAA894 /* ViewTreeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7A2A6EE1BA00AAA894 /* ViewTreeSnapshotTests.swift */; }; - 61054FC32A6EE1BA00AAA894 /* PrivacyLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7B2A6EE1BA00AAA894 /* PrivacyLevelTests.swift */; }; + 61054FC32A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7B2A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift */; }; 61054FC42A6EE1BA00AAA894 /* RecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7C2A6EE1BA00AAA894 /* RecorderTests.swift */; }; 61054FC52A6EE1BA00AAA894 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */; }; 61054FC62A6EE1BA00AAA894 /* CoreGraphicsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */; }; @@ -244,7 +283,6 @@ 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB32423979B00786299 /* DataUploadDelay.swift */; }; 61133C00242397DA00786299 /* DatadogObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133BF2242397DA00786299 /* DatadogObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C092423983800786299 /* Datadog+objc.swift */; }; - 61133C0F2423983800786299 /* ObjcIntercompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0B2423983800786299 /* ObjcIntercompatibility.swift */; }; 61133C102423983800786299 /* Logs+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0C2423983800786299 /* Logs+objc.swift */; }; 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */; }; 61133C482423990D00786299 /* DDDatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C142423990D00786299 /* DDDatadogTests.swift */; }; @@ -270,8 +308,12 @@ 61133C702423993200786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; + 61193AAE2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */; }; + 61193AAF2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */; }; 6121627C247D220500AC5D67 /* TracingWithLoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */; }; 61216B762666DDA10089DCD1 /* LoggerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B752666DDA10089DCD1 /* LoggerConfigurationTests.swift */; }; 61216B7B2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B7A2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift */; }; @@ -346,8 +388,6 @@ 6147E3B3270486920092BC9F /* TraceConfigurationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6147E3B2270486920092BC9F /* TraceConfigurationE2ETests.swift */; }; 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */; }; 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */; }; - 614B78ED296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; - 614B78EE296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; 614B78F1296D7B63009C6B92 /* LowPowerModePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EC296D7B63009C6B92 /* LowPowerModePublisherTests.swift */; }; 614B78F2296D7B63009C6B92 /* LowPowerModePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EC296D7B63009C6B92 /* LowPowerModePublisherTests.swift */; }; 614CADD72510BAC000B93D2D /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614CADD62510BAC000B93D2D /* Environment.swift */; }; @@ -370,6 +410,14 @@ 615CC40C2694A56D0005F08C /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */; }; 615CC4102694A64D0005F08C /* SwiftExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */; }; 615CC4132695957C0005F08C /* CrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC4122695957C0005F08C /* CrashReportTests.swift */; }; + 615D52B82C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */; }; + 615D52B92C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */; }; + 615D52BB2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */; }; + 615D52BC2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */; }; + 615D52BE2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */; }; + 615D52BF2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */; }; + 615D52C12C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */; }; + 615D52C22C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */; }; 6167C79326665D6900D4CF07 /* E2EUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167C79226665D6900D4CF07 /* E2EUtils.swift */; }; 6167C7952666622800D4CF07 /* LoggingE2EHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167C7942666622800D4CF07 /* LoggingE2EHelpers.swift */; }; 6167E6D32B7F8B3300C3CA2D /* AppHangsMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D22B7F8B3300C3CA2D /* AppHangsMonitor.swift */; }; @@ -531,8 +579,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 +683,31 @@ 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 */; }; + 960B26C02D0360EE00D7196F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 960B26BF2D0360EE00D7196F /* Assets.xcassets */; }; + 960B26C32D075BD200D7196F /* DisplayListReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 960B26C22D075BD200D7196F /* DisplayListReflectionTests.swift */; }; + 962C41A72CA431370050B747 /* SessionReplayPrivacyOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */; }; + 962C41A82CA431AA0050B747 /* DDSessionReplayOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E863752C9C7E800023BF78 /* DDSessionReplayOverridesTests.swift */; }; + 962C41A92CB00FD60050B747 /* DDSessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */; }; + 962D72BC2CF6436700F86EF0 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BA2CF6436600F86EF0 /* Image.swift */; }; + 962D72BD2CF6436700F86EF0 /* Image+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */; }; + 962D72BF2CF7538800F86EF0 /* CGImage+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */; }; + 962D72C52CF7806300F86EF0 /* GraphicsImageReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C42CF7806300F86EF0 /* GraphicsImageReflectionTests.swift */; }; + 962D72C72CF7815300F86EF0 /* ReflectionMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */; }; + 96867B992D08826B004AE0BC /* TextReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96867B982D08826B004AE0BC /* TextReflectionTests.swift */; }; + 96867B9B2D0883DD004AE0BC /* ColorReflectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96867B9A2D0883DD004AE0BC /* ColorReflectionTests.swift */; }; + 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */; }; + 969B3B232C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */; }; + 96D331ED2CFF740700649EE8 /* GraphicImagePrivacyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96D331EC2CFF740700649EE8 /* GraphicImagePrivacyTests.swift */; }; + 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */; }; + 96E414162C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E414152C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift */; }; + 96E863722C9C547B0023BF78 /* SessionReplayOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E863712C9C547B0023BF78 /* SessionReplayOverrideTests.swift */; }; + 96F25A822CC7EA4400459567 /* SessionReplayPrivacyOverrides+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F25A802CC7EA4300459567 /* SessionReplayPrivacyOverrides+objc.swift */; }; + 96F25A832CC7EA4400459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F25A812CC7EA4300459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift */; }; + 96F25A852CC7EB3700459567 /* PrivacyOverridesMock+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F25A842CC7EB3700459567 /* PrivacyOverridesMock+objc.swift */; }; + 96F69D6C2CBE94A800A6178B /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; + 96F69D6D2CBE94A900A6178B /* DatadogCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */; }; + 96F69D6E2CBE94F500A6178B /* MockFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71265852B17980C007D63CE /* MockFeature.swift */; }; + 96F69D6F2CBE94F600A6178B /* MockFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71265852B17980C007D63CE /* MockFeature.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 */; }; @@ -652,7 +723,6 @@ A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; A70ADCD22B583B1300321BC9 /* UIImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70ADCD12B583B1300321BC9 /* UIImageResource.swift */; }; A71013D62B178FAD00101E60 /* ResourcesWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */; }; - A71265862B17980C007D63CE /* MockFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71265852B17980C007D63CE /* MockFeature.swift */; }; A727C4BB2BADB3AB00707DFD /* DDSessionReplay+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A795069D2B974CAA00AC4814 /* DDSessionReplay+apiTests.m */; }; A728ADAB2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */; }; A728ADAC2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */; }; @@ -687,6 +757,8 @@ A7F651302B7655DE004B0EDB /* UIImageResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */; }; A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */; }; A7FA98CF2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */; }; + B3C27A082CE6342C006580F9 /* DeterministicSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C27A072CE6342C006580F9 /* DeterministicSamplerTests.swift */; }; + B3C27A092CE6342C006580F9 /* DeterministicSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C27A072CE6342C006580F9 /* DeterministicSamplerTests.swift */; }; D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */; }; D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; }; D20605A4287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; }; @@ -702,8 +774,6 @@ D20605BA2875729E0047275C /* ContextValuePublisherMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B82875729E0047275C /* ContextValuePublisherMock.swift */; }; D20605C42875895C0047275C /* KronosClockMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605BB28757BFB0047275C /* KronosClockMock.swift */; }; D20605C52875895E0047275C /* KronosClockMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605BB28757BFB0047275C /* KronosClockMock.swift */; }; - D20605CA2875A83D0047275C /* ContextValueReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605C62875A77D0047275C /* ContextValueReader.swift */; }; - D20605CB2875A83F0047275C /* ContextValueReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605C62875A77D0047275C /* ContextValueReader.swift */; }; D206BB852A41CA6800F43BA2 /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; D206BB8A2A41CA7000F43BA2 /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D20731B429A5279D00ECBF94 /* DatadogLogs.framework */; }; D207318429A5226B00ECBF94 /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; platformFilter = ios; }; @@ -729,6 +799,7 @@ D20FD9D12ACC02F9004D3569 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; D20FD9D32ACC08D1004D3569 /* WebKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FD9D22ACC08D1004D3569 /* WebKitMocks.swift */; }; D20FD9D62ACC0934004D3569 /* WebLogIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FD9D52ACC0934004D3569 /* WebLogIntegrationTests.swift */; }; + D21331C12D132F0600E4A6A1 /* SwiftUIWireframesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21331C02D132F0600E4A6A1 /* SwiftUIWireframesBuilderTests.swift */; }; D214DA8129DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DE333525C8278A008E3EC2 /* CrashReportingPlugin.swift */; }; D214DA8229DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DE333525C8278A008E3EC2 /* CrashReportingPlugin.swift */; }; D214DA8329DF2D5E004D0AE8 /* CrashReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6161247825CA9CA6009901BE /* CrashReporting.swift */; }; @@ -781,6 +852,8 @@ D2181A8F2B051B7900A518C0 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2181A8D2B051B7900A518C0 /* URLSessionSwizzlerTests.swift */; }; D21831552B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21831542B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift */; }; D21831562B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21831542B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift */; }; + D218B0462D072C8400E3F82C /* SessionReplayTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D218B0452D072C8400E3F82C /* SessionReplayTelemetry.swift */; }; + D218B0482D072CF300E3F82C /* SessionReplayTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D218B0472D072CF300E3F82C /* SessionReplayTelemetryTests.swift */; }; D21A94F22B8397CA00AC4256 /* WebViewMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21A94F12B8397CA00AC4256 /* WebViewMessage.swift */; }; D21A94F32B8397CA00AC4256 /* WebViewMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21A94F12B8397CA00AC4256 /* WebViewMessage.swift */; }; D21AE6BC29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21AE6BB29E5EDAF0064BF29 /* TelemetryTests.swift */; }; @@ -793,6 +866,7 @@ D2216EC12A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2216EBF2A94DE2800ADAEC8 /* FeatureBaggage.swift */; }; D2216EC32A96649500ADAEC8 /* FeatureBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2216EC22A96632F00ADAEC8 /* FeatureBaggageTests.swift */; }; D2216EC42A96649700ADAEC8 /* FeatureBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2216EC22A96632F00ADAEC8 /* FeatureBaggageTests.swift */; }; + D22442C52CA301DA002E71E4 /* UIColor+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */; }; D224430429E9588100274EC7 /* TelemetryReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */; }; D224430529E9588500274EC7 /* TelemetryReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */; }; D224430629E95C2C00274EC7 /* MessageBus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214DAA429E072D7004D0AE8 /* MessageBus.swift */; }; @@ -821,6 +895,8 @@ D22743EA29DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */; }; D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */; }; + D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; + D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */; }; D22C5BC82A98A0B20024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BC52A989D130024CC1F /* Baggages.swift */; }; D22C5BC92A98A0B30024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BC52A989D130024CC1F /* Baggages.swift */; }; D22C5BCB2A98A5400024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BCA2A98A5400024CC1F /* Baggages.swift */; }; @@ -938,7 +1014,7 @@ D23F8E8B29DDCD28001CFAE8 /* RUMContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63824BF19B4008053F2 /* RUMContext.swift */; }; D23F8E8C29DDCD28001CFAE8 /* RUMBaggageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2F329CC88060063802D /* RUMBaggageKeys.swift */; }; D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */; }; - D23F8E8E29DDCD28001CFAE8 /* UIKitRUMUserActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */; }; + D23F8E8E29DDCD28001CFAE8 /* UIEventCommandFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */; }; D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFD824C7269500589570 /* RUMUUIDGenerator.swift */; }; D23F8EA029DDCD38001CFAE8 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */; }; D23F8EA229DDCD38001CFAE8 /* RUMSessionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */; }; @@ -946,7 +1022,6 @@ D23F8EA529DDCD38001CFAE8 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FDF29DDC75A005C54A4 /* UIKitMocks.swift */; }; D23F8EA629DDCD38001CFAE8 /* RUMDeviceInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */; }; D23F8EA829DDCD38001CFAE8 /* RUMResourceScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */; }; - D23F8EA929DDCD38001CFAE8 /* RUMOperatingSystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */; }; D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E820425A879AF0084B751 /* RUMDataModelMocks.swift */; }; D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715FB24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift */; }; D23F8EAD29DDCD38001CFAE8 /* RUMEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */; }; @@ -958,7 +1033,7 @@ D23F8EB329DDCD38001CFAE8 /* ErrorMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */; }; D23F8EB429DDCD38001CFAE8 /* RUMApplicationScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */; }; D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */; }; - D23F8EB829DDCD38001CFAE8 /* UIKitRUMUserActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift */; }; + D23F8EB829DDCD38001CFAE8 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; }; D23F8EB929DDCD38001CFAE8 /* RUMFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333024B75DFC003D6C4E /* RUMFeatureMocks.swift */; }; D23F8EBA29DDCD38001CFAE8 /* ViewIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */; }; D23F8EBE29DDCD38001CFAE8 /* WebViewEventReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53889B2773C4B300A7DC42 /* WebViewEventReceiverTests.swift */; }; @@ -1095,6 +1170,7 @@ D270CDDE2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */; }; D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */; }; D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CBD992BB5DBBB00C766AA /* Mocks.swift */; }; @@ -1106,8 +1182,13 @@ 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 */; }; + D28ABFD32CEB87C600623F27 /* UIHostingViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28ABFD22CEB87C600623F27 /* UIHostingViewRecorderTests.swift */; }; + D28ABFD62CECDE6B00623F27 /* URLSessionInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28ABFD52CECDE6B00623F27 /* URLSessionInterceptorTests.swift */; }; + D28ABFD72CECDE6B00623F27 /* URLSessionInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28ABFD52CECDE6B00623F27 /* URLSessionInterceptorTests.swift */; }; D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */; }; D28F836629C9E6A200EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */; }; D28F836829C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */; }; @@ -1153,7 +1234,7 @@ D29A9F6629DD85BB005C54A4 /* RUMUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B0A4A24EBC43D00A2A780 /* RUMUser.swift */; }; D29A9F6729DD85BB005C54A4 /* RUMOperatingSystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */; }; D29A9F6829DD85BB005C54A4 /* RUMContextAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CBC26D294395A300134409 /* RUMContextAttributes.swift */; }; - D29A9F6929DD85BB005C54A4 /* UIKitRUMUserActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */; }; + D29A9F6929DD85BB005C54A4 /* UIEventCommandFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */; }; D29A9F6A29DD85BB005C54A4 /* SwiftUIViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */; }; D29A9F6B29DD85BB005C54A4 /* VitalInfoSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */; }; D29A9F6D29DD85BB005C54A4 /* UIApplicationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */; }; @@ -1201,9 +1282,8 @@ D29A9FA729DDB483005C54A4 /* RUMCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */; }; D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */; }; D29A9FAB29DDB483005C54A4 /* RUMUserActionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */; }; - D29A9FAC29DDB483005C54A4 /* UIKitRUMUserActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift */; }; + D29A9FAC29DDB483005C54A4 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; }; D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */; }; - D29A9FB029DDB483005C54A4 /* RUMOperatingSystemInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */; }; D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */; }; D29A9FB729DDB483005C54A4 /* ViewIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */; }; D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */; }; @@ -1228,12 +1308,12 @@ D29A9FDA29DDC6D0005C54A4 /* RUMEventFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */; }; D29A9FDB29DDC6D1005C54A4 /* RUMEventFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */; }; D29A9FE029DDC75A005C54A4 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FDF29DDC75A005C54A4 /* UIKitMocks.swift */; }; + D29C9F692D00739400CD568E /* Reflector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C9F682D00739400CD568E /* Reflector.swift */; }; + D29C9F6B2D01D5F600CD568E /* ReflectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C9F6A2D01D5F600CD568E /* ReflectorTests.swift */; }; D29CDD3228211A2200F7DAA5 /* TLVBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */; }; D29CDD3328211A2200F7DAA5 /* TLVBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */; }; D2A1EE23287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */; }; D2A1EE24287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */; }; - D2A1EE26287C35DE00D28DFB /* ContextValueReaderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */; }; - D2A1EE27287C35DE00D28DFB /* ContextValueReaderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */; }; D2A1EE32287DA51900D28DFB /* UserInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */; }; D2A1EE33287DA51900D28DFB /* UserInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */; }; D2A1EE35287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */; }; @@ -1246,9 +1326,6 @@ D2A1EE3F2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */; }; D2A1EE442886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */; }; D2A1EE452886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */; }; - D2A434A22A8E3F900028E329 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; - D2A434AA2A8E40A20028E329 /* SessionReplay+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A434A82A8E402B0028E329 /* SessionReplay+objc.swift */; }; - D2A434AE2A8E426C0028E329 /* DDSessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */; }; D2A783D429A5309F003B03BB /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BBA2423979B00786299 /* SwiftExtensions.swift */; }; D2A783D529A530A0003B03BB /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BBA2423979B00786299 /* SwiftExtensions.swift */; }; D2A783D929A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; @@ -1278,6 +1355,17 @@ D2A7A9002BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9022BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; D2A7A9032BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */; }; + D2AD1CC22CE4AE6600106C74 /* CustomDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBB2CE4AE6600106C74 /* CustomDump.swift */; }; + D2AD1CC32CE4AE6600106C74 /* Color+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */; }; + D2AD1CC42CE4AE6600106C74 /* DisplayList+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBD2CE4AE6600106C74 /* DisplayList+Reflection.swift */; }; + D2AD1CC52CE4AE6600106C74 /* DisplayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */; }; + D2AD1CC62CE4AE6600106C74 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CB92CE4AE6600106C74 /* Color.swift */; }; + D2AD1CC72CE4AE6600106C74 /* Text+Reflection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CC02CE4AE6600106C74 /* Text+Reflection.swift */; }; + D2AD1CC82CE4AE6600106C74 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBF2CE4AE6600106C74 /* Text.swift */; }; + D2AD1CC92CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CBE2CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift */; }; + D2AD1CCC2CE4AE9800106C74 /* UIHostingViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CCB2CE4AE9800106C74 /* UIHostingViewRecorder.swift */; }; + D2AD1CCF2CE4AEF600106C74 /* ReflectionMirrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AD1CCE2CE4AEF600106C74 /* ReflectionMirrorTests.swift */; }; + D2AE9A5D2CF8837C00695264 /* FeatureFlagsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2AE9A5C2CF8836D00695264 /* FeatureFlagsMock.swift */; }; D2B249942A4598FE00DD4F9F /* LoggerProtocol+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B249932A4598FE00DD4F9F /* LoggerProtocol+Internal.swift */; }; D2B249952A4598FE00DD4F9F /* LoggerProtocol+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B249932A4598FE00DD4F9F /* LoggerProtocol+Internal.swift */; }; D2B249972A45E10500DD4F9F /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B249962A45E10500DD4F9F /* LoggerTests.swift */; }; @@ -1396,7 +1484,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 +1517,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 */; }; @@ -1478,7 +1564,6 @@ D2CB6F8427C520D400A62B57 /* DatadogTestsObserverLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 6184751726EFD03400C7C9C5 /* DatadogTestsObserverLoader.m */; }; D2CB6F8527C520D400A62B57 /* PerformancePresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61345612244756E300E7DA6B /* PerformancePresetTests.swift */; }; D2CB6F9627C5217A00A62B57 /* DatadogObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133BF2242397DA00786299 /* DatadogObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2CB6F9827C5217A00A62B57 /* ObjcIntercompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0B2423983800786299 /* ObjcIntercompatibility.swift */; }; D2CB6F9927C5217A00A62B57 /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF5024A49F7400D7BD17 /* Casting.swift */; }; D2CB6F9A27C5217A00A62B57 /* RUMDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */; }; D2CB6F9B27C5217A00A62B57 /* DDSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */; }; @@ -1595,6 +1680,7 @@ D2DC4BF727F484AA00E4FB96 /* DataEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DC4BF527F484AA00E4FB96 /* DataEncryption.swift */; }; D2DE63532A30A7CA00441A54 /* CoreRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */; }; D2DE63542A30A7CA00441A54 /* CoreRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */; }; + D2EA0F432C0D941900CB20F8 /* ReflectionMirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EA0F422C0D941900CB20F8 /* ReflectionMirror.swift */; }; D2EA0F462C0E1AE300CB20F8 /* SessionReplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EA0F452C0E1AE200CB20F8 /* SessionReplayConfiguration.swift */; }; D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E13A92524B8700098C6B0 /* HTTPHeadersReader.swift */; }; D2EBEE2029BA160F00B15732 /* TracePropagationHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDCF29B8A02100B15732 /* TracePropagationHeadersWriter.swift */; }; @@ -1656,6 +1742,18 @@ E1C853142AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */; }; E1C853152AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */; }; E1D5AEA724B4D45B007F194B /* Versioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5AEA624B4D45A007F194B /* Versioning.swift */; }; + E2AA55E72C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */; }; + E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */; }; + E2AA55EA2C32C76A002FEF28 /* WatchKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */; }; + E2AA55EC2C32C78B002FEF28 /* WatchKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */; }; + F603F1262CAE9F760088E6B7 /* DDInternalLogger+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F603F1252CAE9F760088E6B7 /* DDInternalLogger+objc.swift */; }; + F603F1272CAE9F760088E6B7 /* DDInternalLogger+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F603F1252CAE9F760088E6B7 /* DDInternalLogger+objc.swift */; }; + F603F12B2CAEA4FA0088E6B7 /* DDInternalLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F603F1282CAEA4E90088E6B7 /* DDInternalLoggerTests.swift */; }; + F603F12C2CAEA7180088E6B7 /* DDInternalLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F603F1282CAEA4E90088E6B7 /* DDInternalLoggerTests.swift */; }; + F603F1302CAEA7620088E6B7 /* DDInternalLogger+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F603F12D2CAEA7590088E6B7 /* DDInternalLogger+apiTests.m */; }; + F603F1312CAEA7630088E6B7 /* DDInternalLogger+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F603F12D2CAEA7590088E6B7 /* DDInternalLogger+apiTests.m */; }; + F6E106542C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */; }; + F6E106552C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2062,7 +2160,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 116F84052CFDD06700705755 /* SampleRateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleRateTests.swift; sourceTree = ""; }; 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,8 +2176,18 @@ 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 = ""; }; + 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Crypto.swift"; sourceTree = ""; }; + 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+CryptoTests.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 = ""; }; + 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitorTests.swift; sourceTree = ""; }; + 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMocks.swift; sourceTree = ""; }; + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitor.swift; sourceTree = ""; }; + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarning.swift; sourceTree = ""; }; + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningReporter.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 = ""; }; + 3C62C3602C3E852F00C7E336 /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = ""; }; 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanBuilder.swift; sourceTree = ""; }; 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+Datadog.swift"; sourceTree = ""; }; @@ -2087,6 +2198,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 +2213,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 = ""; }; @@ -2118,10 +2237,10 @@ 61054E112A6EE10A00AAA894 /* Recorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; 61054E122A6EE10A00AAA894 /* PrivacyLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyLevel.swift; sourceTree = ""; }; 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+SessionReplay.swift"; sourceTree = ""; }; - 61054E152A6EE10A00AAA894 /* UIKitExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; + 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplay.swift"; sourceTree = ""; }; 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CFType+Safety.swift"; sourceTree = ""; }; 61054E172A6EE10A00AAA894 /* SystemColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemColors.swift; sourceTree = ""; }; - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrame.swift"; sourceTree = ""; }; + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplay.swift"; sourceTree = ""; }; 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinator.swift; sourceTree = ""; }; 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzler.swift; sourceTree = ""; }; 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchSnapshotProducer.swift; sourceTree = ""; }; @@ -2147,7 +2266,6 @@ 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITabBarRecorder.swift; sourceTree = ""; }; 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISegmentRecorder.swift; sourceTree = ""; }; 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsupportedViewRecorder.swift; sourceTree = ""; }; - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ViewAttributes+Copy.swift"; sourceTree = ""; }; 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContext.swift; sourceTree = ""; }; 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeIDGenerator.swift; sourceTree = ""; }; 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowViewTreeSnapshotProducer.swift; sourceTree = ""; }; @@ -2165,14 +2283,13 @@ 61054E512A6EE10A00AAA894 /* WireframesBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WireframesBuilder.swift; sourceTree = ""; }; 61054E532A6EE10A00AAA894 /* NodesFlattener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesFlattener.swift; sourceTree = ""; }; 61054E552A6EE10A00AAA894 /* CGRectExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGRectExtensions.swift; sourceTree = ""; }; - 61054E562A6EE10A00AAA894 /* UIImage+Scaling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; 61054E582A6EE10A00AAA894 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; }; 61054E592A6EE10A00AAA894 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 61054E5A2A6EE10A00AAA894 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 61054E5C2A6EE10A00AAA894 /* MainThreadScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainThreadScheduler.swift; sourceTree = ""; }; 61054E5D2A6EE10A00AAA894 /* Scheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = ""; }; 61054F3D2A6EE1B900AAA894 /* SessionReplayConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayConfigurationTests.swift; sourceTree = ""; }; - 61054F402A6EE1B900AAA894 /* UIImage+ScalingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+ScalingTests.swift"; sourceTree = ""; }; + 61054F402A6EE1B900AAA894 /* UIImage+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+SessionReplayTests.swift"; sourceTree = ""; }; 61054F412A6EE1B900AAA894 /* CGRectExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGRectExtensionsTests.swift; sourceTree = ""; }; 61054F422A6EE1B900AAA894 /* ColorsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorsTests.swift; sourceTree = ""; }; 61054F432A6EE1B900AAA894 /* CFType+SafetyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CFType+SafetyTests.swift"; sourceTree = ""; }; @@ -2189,8 +2306,8 @@ 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessorTests.swift; sourceTree = ""; }; 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesFlattenerTests.swift; sourceTree = ""; }; 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinatorTests.swift; sourceTree = ""; }; - 61054F5D2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitExtensionsTests.swift; sourceTree = ""; }; - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrameTests.swift"; sourceTree = ""; }; + 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplayTests.swift"; sourceTree = ""; }; + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplayTests.swift"; sourceTree = ""; }; 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowTouchSnapshotProducerTests.swift; sourceTree = ""; }; 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchIdentifierGeneratorTests.swift; sourceTree = ""; }; 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContextTests.swift; sourceTree = ""; }; @@ -2213,7 +2330,7 @@ 61054F782A6EE1BA00AAA894 /* UITextViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextViewRecorderTests.swift; sourceTree = ""; }; 61054F792A6EE1BA00AAA894 /* ViewTreeRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecorderTests.swift; sourceTree = ""; }; 61054F7A2A6EE1BA00AAA894 /* ViewTreeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeSnapshotTests.swift; sourceTree = ""; }; - 61054F7B2A6EE1BA00AAA894 /* PrivacyLevelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyLevelTests.swift; sourceTree = ""; }; + 61054F7B2A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAndInputPrivacyLevelTests.swift; sourceTree = ""; }; 61054F7C2A6EE1BA00AAA894 /* RecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderTests.swift; sourceTree = ""; }; 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphicsMocks.swift; sourceTree = ""; }; @@ -2260,7 +2377,6 @@ 61133BF2242397DA00786299 /* DatadogObjc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DatadogObjc.h; sourceTree = ""; }; 61133BF3242397DA00786299 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61133C092423983800786299 /* Datadog+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Datadog+objc.swift"; sourceTree = ""; }; - 61133C0B2423983800786299 /* ObjcIntercompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcIntercompatibility.swift; sourceTree = ""; }; 61133C0C2423983800786299 /* Logs+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logs+objc.swift"; sourceTree = ""; }; 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DatadogConfiguration+objc.swift"; sourceTree = ""; }; 61133C142423990D00786299 /* DDDatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDDatadogTests.swift; sourceTree = ""; }; @@ -2290,7 +2406,9 @@ 611529A425E3DD51004F740E /* ValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisher.swift; sourceTree = ""; }; 611529AD25E3E429004F740E /* ValuePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisherTests.swift; sourceTree = ""; }; 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionDelegate+objc.swift"; sourceTree = ""; }; + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifierTests.swift; sourceTree = ""; }; + 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandler.swift; sourceTree = ""; }; 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; 61216275247D1CD700AC5D67 /* TracingWithLoggingIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingWithLoggingIntegration.swift; sourceTree = ""; }; 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingWithLoggingIntegrationTests.swift; sourceTree = ""; }; @@ -2339,7 +2457,7 @@ 613F9C172BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogCore+FeatureDataStoreTests.swift"; sourceTree = ""; }; 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureScopeMock.swift; sourceTree = ""; }; 6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzler.swift; sourceTree = ""; }; - 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMUserActionsHandler.swift; sourceTree = ""; }; + 6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEventCommandFactory.swift; sourceTree = ""; }; 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzlerTests.swift; sourceTree = ""; }; 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Casting+RUM.swift"; sourceTree = ""; }; 614396712A67D74F00197326 /* BatchMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchMetrics.swift; sourceTree = ""; }; @@ -2383,10 +2501,14 @@ 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTSpanContext+objc.swift"; sourceTree = ""; }; 615B0F8A2BB33C2800E9ED6C /* AppHangsMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsMonitorTests.swift; sourceTree = ""; }; 615B0F8D2BB33E0400E9ED6C /* DataStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreMock.swift; sourceTree = ""; }; - 615C3195251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMUserActionsHandlerTests.swift; sourceTree = ""; }; + 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandlerTests.swift; sourceTree = ""; }; 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionTests.swift; sourceTree = ""; }; 615CC4122695957C0005F08C /* CrashReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportTests.swift; sourceTree = ""; }; + 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedAttributes.swift; sourceTree = ""; }; + 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTags.swift; sourceTree = ""; }; + 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTagsTests.swift; sourceTree = ""; }; + 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedAttributesTests.swift; sourceTree = ""; }; 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogCrashReporting.xcconfig; sourceTree = ""; }; 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; 6161247825CA9CA6009901BE /* CrashReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = ""; }; @@ -2417,7 +2539,6 @@ 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceSamplingStrategy+objc.swift"; sourceTree = ""; }; 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMMonitorTests.swift; sourceTree = ""; }; 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfo.swift; sourceTree = ""; }; - 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfoTests.swift; sourceTree = ""; }; 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMCommandSubscriber.swift; sourceTree = ""; }; 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMInstrumentation.swift; sourceTree = ""; }; 616F1FAF283E227100651A3A /* LogsFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsFeature.swift; sourceTree = ""; }; @@ -2537,8 +2658,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 +2771,26 @@ 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 = ""; }; + 960B26BF2D0360EE00D7196F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 960B26C22D075BD200D7196F /* DisplayListReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayListReflectionTests.swift; sourceTree = ""; }; + 962D72BA2CF6436600F86EF0 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+Reflection.swift"; sourceTree = ""; }; + 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+SessionReplay.swift"; sourceTree = ""; }; + 962D72C42CF7806300F86EF0 /* GraphicsImageReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicsImageReflectionTests.swift; sourceTree = ""; }; + 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflectionMocks.swift; sourceTree = ""; }; + 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayPrivacyOverrides.swift; sourceTree = ""; }; + 96867B982D08826B004AE0BC /* TextReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextReflectionTests.swift; sourceTree = ""; }; + 96867B9A2D0883DD004AE0BC /* ColorReflectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorReflectionTests.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 = ""; }; + 96D331EC2CFF740700649EE8 /* GraphicImagePrivacyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicImagePrivacyTests.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 = ""; }; + 96E863712C9C547B0023BF78 /* SessionReplayOverrideTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayOverrideTests.swift; sourceTree = ""; }; + 96E863752C9C7E800023BF78 /* DDSessionReplayOverridesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSessionReplayOverridesTests.swift; sourceTree = ""; }; + 96F25A802CC7EA4300459567 /* SessionReplayPrivacyOverrides+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionReplayPrivacyOverrides+objc.swift"; sourceTree = ""; }; + 96F25A812CC7EA4300459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplayPrivacyOverrides+objc.swift"; sourceTree = ""; }; + 96F25A842CC7EB3700459567 /* PrivacyOverridesMock+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PrivacyOverridesMock+objc.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 = ""; }; @@ -2714,6 +2853,7 @@ A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodCalledMetric.swift; sourceTree = ""; }; B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReader.swift; sourceTree = ""; }; B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReaderTests.swift; sourceTree = ""; }; + B3C27A072CE6342C006580F9 /* DeterministicSamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterministicSamplerTests.swift; sourceTree = ""; }; B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalInfo.swift; sourceTree = ""; }; B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoTests.swift; sourceTree = ""; }; D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireframesBuilderTests.swift; sourceTree = ""; }; @@ -2724,13 +2864,13 @@ D20605B5287572640047275C /* DatadogContextProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogContextProviderMock.swift; sourceTree = ""; }; D20605B82875729E0047275C /* ContextValuePublisherMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValuePublisherMock.swift; sourceTree = ""; }; D20605BB28757BFB0047275C /* KronosClockMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KronosClockMock.swift; sourceTree = ""; }; - D20605C62875A77D0047275C /* ContextValueReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValueReader.swift; sourceTree = ""; }; D207317C29A5226A00ECBF94 /* DatadogLogs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogLogs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D207318329A5226A00ECBF94 /* DatadogLogsTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogLogsTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D20731B429A5279D00ECBF94 /* DatadogLogs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogLogs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D20FD9CE2AC6FF42004D3569 /* WebViewLogReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewLogReceiverTests.swift; sourceTree = ""; }; D20FD9D22ACC08D1004D3569 /* WebKitMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitMocks.swift; sourceTree = ""; }; D20FD9D52ACC0934004D3569 /* WebLogIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLogIntegrationTests.swift; sourceTree = ""; }; + D21331C02D132F0600E4A6A1 /* SwiftUIWireframesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIWireframesBuilderTests.swift; sourceTree = ""; }; D213532F270CA722000315AD /* DataCompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompressionTests.swift; sourceTree = ""; }; D214DAA429E072D7004D0AE8 /* MessageBus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBus.swift; sourceTree = ""; }; D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiver.swift; sourceTree = ""; }; @@ -2754,6 +2894,8 @@ D2181A8A2B0500BB00A518C0 /* NetworkInstrumentationSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationSwizzler.swift; sourceTree = ""; }; D2181A8D2B051B7900A518C0 /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; D21831542B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationIntegrationTests.swift; sourceTree = ""; }; + D218B0452D072C8400E3F82C /* SessionReplayTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayTelemetry.swift; sourceTree = ""; }; + D218B0472D072CF300E3F82C /* SessionReplayTelemetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayTelemetryTests.swift; sourceTree = ""; }; D21A94F12B8397CA00AC4256 /* WebViewMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewMessage.swift; sourceTree = ""; }; D21AE6BB29E5EDAF0064BF29 /* TelemetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryTests.swift; sourceTree = ""; }; D21C26C428A3B49C005DD405 /* FeatureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureStorage.swift; sourceTree = ""; }; @@ -2763,8 +2905,10 @@ D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageReceiverTests.swift; sourceTree = ""; }; D2216EBF2A94DE2800ADAEC8 /* FeatureBaggage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureBaggage.swift; sourceTree = ""; }; D2216EC22A96632F00ADAEC8 /* FeatureBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureBaggageTests.swift; sourceTree = ""; }; + D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+SessionReplay.swift"; sourceTree = ""; }; D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReportReceiverTests.swift; sourceTree = ""; }; D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataModelMocks.swift; sourceTree = ""; }; + D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = ""; }; D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = ""; }; D22C5BC52A989D130024CC1F /* Baggages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baggages.swift; sourceTree = ""; }; D22C5BCA2A98A5400024CC1F /* Baggages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baggages.swift; sourceTree = ""; }; @@ -2877,9 +3021,13 @@ D26C49BE288982DA00802B2D /* FeatureUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureUpload.swift; sourceTree = ""; }; D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+SessionReplay.swift"; sourceTree = ""; }; D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; D27CBD992BB5DBBB00C766AA /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionTests.swift; sourceTree = ""; }; D286626D2A43487500852CE3 /* Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = ""; }; + D28ABFD22CEB87C600623F27 /* UIHostingViewRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingViewRecorderTests.swift; sourceTree = ""; }; + D28ABFD52CECDE6B00623F27 /* URLSessionInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionInterceptorTests.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 = ""; }; D28FCC342B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -2898,10 +3046,11 @@ D29A9FCB29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTAssertValidRUMUUID.swift; sourceTree = ""; }; D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMFeatureMocks.swift; sourceTree = ""; }; D29A9FDF29DDC75A005C54A4 /* UIKitMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; + D29C9F682D00739400CD568E /* Reflector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reflector.swift; sourceTree = ""; }; + D29C9F6A2D01D5F600CD568E /* ReflectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflectorTests.swift; sourceTree = ""; }; D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlock.swift; sourceTree = ""; }; D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIActionModifier.swift; sourceTree = ""; }; D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisher.swift; sourceTree = ""; }; - D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValueReaderMock.swift; sourceTree = ""; }; D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoPublisher.swift; sourceTree = ""; }; D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisherTests.swift; sourceTree = ""; }; D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoPublisherTests.swift; sourceTree = ""; }; @@ -2909,12 +3058,22 @@ D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimePublisherTests.swift; sourceTree = ""; }; D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoPublisherTests.swift; sourceTree = ""; }; D2A38DDA29C37E1B007C6900 /* TracingURLSessionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandlerTests.swift; sourceTree = ""; }; - D2A434A82A8E402B0028E329 /* SessionReplay+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionReplay+objc.swift"; sourceTree = ""; }; D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSessionReplayTests.swift; sourceTree = ""; }; D2A7840129A534F9003B03BB /* DatadogLogsTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogLogsTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintFunctionMock.swift; sourceTree = ""; }; D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; + D2AD1CB92CE4AE6600106C74 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Reflection.swift"; sourceTree = ""; }; + D2AD1CBB2CE4AE6600106C74 /* CustomDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDump.swift; sourceTree = ""; }; + D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayList.swift; sourceTree = ""; }; + D2AD1CBD2CE4AE6600106C74 /* DisplayList+Reflection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DisplayList+Reflection.swift"; sourceTree = ""; }; + D2AD1CBE2CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIWireframesBuilder.swift; sourceTree = ""; }; + D2AD1CBF2CE4AE6600106C74 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + D2AD1CC02CE4AE6600106C74 /* Text+Reflection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+Reflection.swift"; sourceTree = ""; }; + D2AD1CCB2CE4AE9800106C74 /* UIHostingViewRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingViewRecorder.swift; sourceTree = ""; }; + D2AD1CCE2CE4AEF600106C74 /* ReflectionMirrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflectionMirrorTests.swift; sourceTree = ""; }; + D2AE9A5C2CF8836D00695264 /* FeatureFlagsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagsMock.swift; sourceTree = ""; }; D2B249932A4598FE00DD4F9F /* LoggerProtocol+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoggerProtocol+Internal.swift"; sourceTree = ""; }; D2B249962A45E10500DD4F9F /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; D2B3F0432823EE8300C2B5EE /* TLVBlockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlockTests.swift; sourceTree = ""; }; @@ -2964,6 +3123,7 @@ D2DC4BF527F484AA00E4FB96 /* DataEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataEncryption.swift; sourceTree = ""; }; D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreRegistry.swift; sourceTree = ""; }; D2E8D59728C7AB90007E5DE1 /* ContextMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMessageReceiverTests.swift; sourceTree = ""; }; + D2EA0F422C0D941900CB20F8 /* ReflectionMirror.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReflectionMirror.swift; sourceTree = ""; }; D2EA0F452C0E1AE200CB20F8 /* SessionReplayConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayConfiguration.swift; sourceTree = ""; }; D2EBEDCC29B893D800B15732 /* TraceID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceID.swift; sourceTree = ""; }; D2EBEDCF29B8A02100B15732 /* TracePropagationHeadersWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracePropagationHeadersWriter.swift; sourceTree = ""; }; @@ -2989,7 +3149,13 @@ E1D202E924C065CF00D1AF3A /* ActiveSpansPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSpansPool.swift; sourceTree = ""; }; E1D203FB24C1884500D1AF3A /* ActiveSpansPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSpansPoolTests.swift; sourceTree = ""; }; E1D5AEA624B4D45A007F194B /* Versioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Versioning.swift; sourceTree = ""; }; + E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationNotifications.swift; sourceTree = ""; }; + E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchKitExtensions.swift; sourceTree = ""; }; + F603F1252CAE9F760088E6B7 /* DDInternalLogger+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDInternalLogger+objc.swift"; sourceTree = ""; }; + F603F1282CAEA4E90088E6B7 /* DDInternalLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDInternalLoggerTests.swift; sourceTree = ""; }; + F603F12D2CAEA7590088E6B7 /* DDInternalLogger+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDInternalLogger+apiTests.m"; sourceTree = ""; }; F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMUserActionsPredicate.swift; sourceTree = ""; }; + F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LogsDataModels+objc.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3033,7 +3199,6 @@ 6147989E2A45A42C0095CB02 /* DatadogTrace.framework in Frameworks */, 61A2CC262A4449210000FF25 /* DatadogRUM.framework in Frameworks */, D206BB852A41CA6800F43BA2 /* DatadogLogs.framework in Frameworks */, - D2A434A22A8E3F900028E329 /* DatadogSessionReplay.framework in Frameworks */, 61133C702423993200786299 /* DatadogCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3360,6 +3525,37 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */ = { + isa = PBXGroup; + children = ( + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */, + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */, + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */, + ); + path = MemoryWarnings; + sourceTree = ""; + }; + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */ = { + isa = PBXGroup; + children = ( + 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */, + 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */, + ); + path = MemoryWarnings; + sourceTree = ""; + }; + 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 +3609,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 = ( @@ -3427,7 +3634,10 @@ children = ( 61054E0C2A6EE10A00AAA894 /* SessionReplay.swift */, 61054E0B2A6EE10A00AAA894 /* SessionReplayConfiguration.swift */, + 966253B52C98807400B90B63 /* SessionReplayPrivacyOverrides.swift */, A795069B2B974C8100AC4814 /* SessionReplay+objc.swift */, + 96F25A802CC7EA4300459567 /* SessionReplayPrivacyOverrides+objc.swift */, + 96F25A812CC7EA4300459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift */, 61054E3B2A6EE10A00AAA894 /* Feature */, 61054E482A6EE10A00AAA894 /* Processor */, 61054E0D2A6EE10A00AAA894 /* Recorder */, @@ -3442,8 +3652,12 @@ 61054E022A6EE0DB00AAA894 /* DatadogSessionReplayTests */ = { isa = PBXGroup; children = ( + 960B26C12D03611400D7196F /* Resources */, + 96E863712C9C547B0023BF78 /* SessionReplayOverrideTests.swift */, 61054F482A6EE1B900AAA894 /* SessionReplayTests.swift */, 61054F3D2A6EE1B900AAA894 /* SessionReplayConfigurationTests.swift */, + D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */, + 96E863752C9C7E800023BF78 /* DDSessionReplayOverridesTests.swift */, 61054F882A6EE1BA00AAA894 /* Feature */, 61054F922A6EE1BA00AAA894 /* Helpers */, 61054F7D2A6EE1BA00AAA894 /* Mocks */, @@ -3493,10 +3707,13 @@ isa = PBXGroup; children = ( 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */, - 61054E152A6EE10A00AAA894 /* UIKitExtensions.swift */, + 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */, + D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */, + 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, 61054E172A6EE10A00AAA894 /* SystemColors.swift */, - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */, + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */, + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */, ); name = Utilities; path = Recorder/Utilities; @@ -3538,7 +3755,6 @@ 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */, 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */, 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */, - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */, 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */, 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */, 61054E272A6EE10A00AAA894 /* NodeRecorders */, @@ -3549,6 +3765,8 @@ 61054E272A6EE10A00AAA894 /* NodeRecorders */ = { isa = PBXGroup; children = ( + D2AD1CC12CE4AE6600106C74 /* SwiftUI */, + 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */, 61054E282A6EE10A00AAA894 /* UIDatePickerRecorder.swift */, 61054E292A6EE10A00AAA894 /* UITextViewRecorder.swift */, 61054E2A2A6EE10A00AAA894 /* UIImageViewRecorder.swift */, @@ -3559,6 +3777,7 @@ 61054E2E2A6EE10A00AAA894 /* NodeRecorder.swift */, 61054E2F2A6EE10A00AAA894 /* UISliderRecorder.swift */, 61054E302A6EE10A00AAA894 /* UIPickerViewRecorder.swift */, + 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */, 61054E312A6EE10A00AAA894 /* UIStepperRecorder.swift */, 61054E322A6EE10A00AAA894 /* UILabelRecorder.swift */, 61054E332A6EE10A00AAA894 /* UISwitchRecorder.swift */, @@ -3566,6 +3785,7 @@ 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */, 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */, D2BCB2A02B7B8107005C2AAB /* WKWebViewRecorder.swift */, + D2AD1CCB2CE4AE9800106C74 /* UIHostingViewRecorder.swift */, ); path = NodeRecorders; sourceTree = ""; @@ -3579,6 +3799,7 @@ D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */, 61054E3F2A6EE10A00AAA894 /* SRContextPublisher.swift */, D22C5BCD2A98A65D0024CC1F /* Baggages.swift */, + D218B0452D072C8400E3F82C /* SessionReplayTelemetry.swift */, 61054E402A6EE10A00AAA894 /* RequestBuilders */, ); path = Feature; @@ -3662,8 +3883,9 @@ isa = PBXGroup; children = ( 61054E552A6EE10A00AAA894 /* CGRectExtensions.swift */, - 61054E562A6EE10A00AAA894 /* UIImage+Scaling.swift */, 61054E582A6EE10A00AAA894 /* Queue.swift */, + D29C9F682D00739400CD568E /* Reflector.swift */, + D2EA0F422C0D941900CB20F8 /* ReflectionMirror.swift */, 61054E592A6EE10A00AAA894 /* Errors.swift */, 61054E5A2A6EE10A00AAA894 /* Colors.swift */, 61054E5B2A6EE10A00AAA894 /* Schedulers */, @@ -3684,7 +3906,7 @@ 61054F3E2A6EE1B900AAA894 /* Utilities */ = { isa = PBXGroup; children = ( - 61054F402A6EE1B900AAA894 /* UIImage+ScalingTests.swift */, + 61054F402A6EE1B900AAA894 /* UIImage+SessionReplayTests.swift */, 61054F412A6EE1B900AAA894 /* CGRectExtensionsTests.swift */, 61054F422A6EE1B900AAA894 /* ColorsTests.swift */, 61054F432A6EE1B900AAA894 /* CFType+SafetyTests.swift */, @@ -3767,7 +3989,7 @@ 61054F5B2A6EE1BA00AAA894 /* Utilties */, 61054F602A6EE1BA00AAA894 /* TouchSnapshotProducer */, 61054F642A6EE1BA00AAA894 /* ViewTreeSnapshotProducer */, - 61054F7B2A6EE1BA00AAA894 /* PrivacyLevelTests.swift */, + 61054F7B2A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift */, 61054F7C2A6EE1BA00AAA894 /* RecorderTests.swift */, ); path = Recorder; @@ -3776,8 +3998,10 @@ 61054F5B2A6EE1BA00AAA894 /* Utilties */ = { isa = PBXGroup; children = ( - 61054F5D2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift */, - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */, + 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */, + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */, + D2AD1CCE2CE4AEF600106C74 /* ReflectionMirrorTests.swift */, + D29C9F6A2D01D5F600CD568E /* ReflectorTests.swift */, ); path = Utilties; sourceTree = ""; @@ -3823,9 +4047,12 @@ 61054F692A6EE1BA00AAA894 /* NodeRecorders */ = { isa = PBXGroup; children = ( + 960B26BA2D03541900D7196F /* SwiftUI */, + 969B3B222C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift */, 61054F6A2A6EE1BA00AAA894 /* UILabelRecorderTests.swift */, 61054F6B2A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift */, 61054F6C2A6EE1BA00AAA894 /* UITabBarRecorderTests.swift */, + 96E414152C2AF5C1005A6119 /* UIProgressViewRecorderTests.swift */, 61054F6D2A6EE1BA00AAA894 /* UISliderRecorderTests.swift */, 61054F6E2A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift */, 61054F6F2A6EE1BA00AAA894 /* UISegmentRecorderTests.swift */, @@ -3840,6 +4067,7 @@ 61054F782A6EE1BA00AAA894 /* UITextViewRecorderTests.swift */, D2BCB2A22B7B9683005C2AAB /* WKWebViewRecorderTests.swift */, A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */, + D28ABFD22CEB87C600623F27 /* UIHostingViewRecorderTests.swift */, ); path = NodeRecorders; sourceTree = ""; @@ -3847,6 +4075,8 @@ 61054F7D2A6EE1BA00AAA894 /* Mocks */ = { isa = PBXGroup; children = ( + D2AE9A5C2CF8836D00695264 /* FeatureFlagsMock.swift */, + 96F25A842CC7EB3700459567 /* PrivacyOverridesMock+objc.swift */, 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */, 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */, 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */, @@ -3859,8 +4089,8 @@ 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */, A74A72862B10CE4100771FEB /* ResourceMocks.swift */, A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, - A71265852B17980C007D63CE /* MockFeature.swift */, A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */, + 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */, ); path = Mocks; sourceTree = ""; @@ -3871,6 +4101,7 @@ 61054F892A6EE1BA00AAA894 /* RUMContextReceiverTests.swift */, 61054F8A2A6EE1BA00AAA894 /* SRContextPublisherTests.swift */, D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */, + D218B0472D072CF300E3F82C /* SessionReplayTelemetryTests.swift */, 61054F8B2A6EE1BA00AAA894 /* RequestBuilder */, ); path = Feature; @@ -4100,7 +4331,6 @@ 61133BB72423979B00786299 /* Utils */ = { isa = PBXGroup; children = ( - 61C3638424361E9200C4D4E6 /* Globals.swift */, 6139CD702589FAFD007E8BB7 /* Retrying.swift */, 61DA8CAE28620C760074A606 /* Cryptography.swift */, ); @@ -4113,6 +4343,8 @@ 61133BC22423979B00786299 /* LogEventEncoder.swift */, 61133BC32423979B00786299 /* LogEventBuilder.swift */, 61133BC42423979B00786299 /* LogEventSanitizer.swift */, + 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */, + 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */, ); path = Log; sourceTree = ""; @@ -4154,21 +4386,12 @@ 6132BF4524A498B400D7BD17 /* Tracing */, 6111C58025C0080C00F5C4A2 /* RUM */, 6132BF4024A38D0600D7BD17 /* OpenTracing */, - D2A434A72A8E3FFB0028E329 /* SessionReplay */, - 61133C0A2423983800786299 /* ObjcIntercompatibility */, + F603F1252CAE9F760088E6B7 /* DDInternalLogger+objc.swift */, ); name = DatadogObjc; path = ../DatadogObjc/Sources; sourceTree = ""; }; - 61133C0A2423983800786299 /* ObjcIntercompatibility */ = { - isa = PBXGroup; - children = ( - 61133C0B2423983800786299 /* ObjcIntercompatibility.swift */, - ); - path = ObjcIntercompatibility; - sourceTree = ""; - }; 61133C122423990D00786299 /* DatadogCoreTests */ = { isa = PBXGroup; children = ( @@ -4200,8 +4423,8 @@ A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */, 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */, 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */, - D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */, 61D03BDE273404BB00367DE0 /* RUM */, + F603F1282CAEA4E90088E6B7 /* DDInternalLoggerTests.swift */, ); path = DatadogObjc; sourceTree = ""; @@ -4243,7 +4466,6 @@ D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */, D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */, 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */, - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, D20605BB28757BFB0047275C /* KronosClockMock.swift */, 61F1A61B2498AD2C00075390 /* SystemFrameworks */, ); @@ -4321,6 +4543,8 @@ children = ( 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */, 61133C3C2423990D00786299 /* LogSanitizerTests.swift */, + 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */, + 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */, ); path = Log; sourceTree = ""; @@ -4510,7 +4734,7 @@ 6141014C251A577D00E3C2D9 /* Actions */ = { isa = PBXGroup; children = ( - 615C3195251DD5080018781C /* UIKitRUMUserActionsHandlerTests.swift */, + 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */, ); path = Actions; sourceTree = ""; @@ -4518,6 +4742,7 @@ 6141014D251A578D00E3C2D9 /* Actions */ = { isa = PBXGroup; children = ( + 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */, D29D5A4A273BF81500A687C1 /* UIKit */, D29D5A4B273BF82200A687C1 /* SwiftUI */, ); @@ -4562,6 +4787,7 @@ 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */, 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */, D2F44FC1299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift */, + 3C62C3602C3E852F00C7E336 /* MultiSelector.swift */, ); path = Utils; sourceTree = ""; @@ -4714,6 +4940,7 @@ 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */, 6167E6F52B81E94C00C3CA2D /* DDThread.swift */, 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */, + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */, ); path = CrashReporting; sourceTree = ""; @@ -4750,6 +4977,7 @@ 616CCE11250A181C009FED46 /* Instrumentation */ = { isa = PBXGroup; children = ( + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */, 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */, 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */, 61F3CDA1251118DD00C816E5 /* Views */, @@ -4757,6 +4985,7 @@ 6157FA5C252767B3009A8A3B /* Resources */, 9E06058F26EF904200F5F935 /* LongTasks */, 6167E6D12B7F8B1300C3CA2D /* AppHangs */, + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */, ); path = Instrumentation; sourceTree = ""; @@ -5118,6 +5347,7 @@ 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */, A795069D2B974CAA00AC4814 /* DDSessionReplay+apiTests.m */, 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */, + F603F12D2CAEA7590088E6B7 /* DDInternalLogger+apiTests.m */, ); path = ObjcAPITests; sourceTree = ""; @@ -5218,6 +5448,7 @@ 61C713C52A3CA08B00FA735A /* CoreMocks */ = { isa = PBXGroup; children = ( + A71265852B17980C007D63CE /* MockFeature.swift */, D257954A298ABB04008A1BE5 /* PassthroughCoreMock.swift */, D2160CEF29C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift */, 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */, @@ -5334,6 +5565,7 @@ children = ( 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */, 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */, + 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */, D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */, 61DCC84C2C05D4E500CB59E5 /* SDKMetrics */, ); @@ -5415,10 +5647,12 @@ 61F3CDA825121F8F00C816E5 /* Instrumentation */ = { isa = PBXGroup; children = ( + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */, 61F3CDA925121FA100C816E5 /* Views */, 6141014C251A577D00E3C2D9 /* Actions */, 613F23EF252B1287006CD2D7 /* Resources */, 6167E6D82B80047900C3CA2D /* AppHangs */, + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */, 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */, ); path = Instrumentation; @@ -5486,7 +5720,6 @@ 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */, 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */, 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */, - 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */, ); path = RUMEvent; sourceTree = ""; @@ -5499,6 +5732,27 @@ path = RUMEventOutputs; sourceTree = ""; }; + 960B26BA2D03541900D7196F /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D21331C02D132F0600E4A6A1 /* SwiftUIWireframesBuilderTests.swift */, + 962D72C42CF7806300F86EF0 /* GraphicsImageReflectionTests.swift */, + 96867B982D08826B004AE0BC /* TextReflectionTests.swift */, + 96867B9A2D0883DD004AE0BC /* ColorReflectionTests.swift */, + 96D331EC2CFF740700649EE8 /* GraphicImagePrivacyTests.swift */, + 960B26C22D075BD200D7196F /* DisplayListReflectionTests.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 960B26C12D03611400D7196F /* Resources */ = { + isa = PBXGroup; + children = ( + 960B26BF2D0360EE00D7196F /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; 9E06058F26EF904200F5F935 /* LongTasks */ = { isa = PBXGroup; children = ( @@ -5609,7 +5863,6 @@ children = ( D20605B5287572640047275C /* DatadogContextProviderMock.swift */, D20605B82875729E0047275C /* ContextValuePublisherMock.swift */, - D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */, ); path = DatadogCore; sourceTree = ""; @@ -5700,6 +5953,14 @@ path = Integrations; sourceTree = ""; }; + D227A0A22C76229400C83324 /* Benchmarks */ = { + isa = PBXGroup; + children = ( + D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */, + ); + path = Benchmarks; + sourceTree = ""; + }; D22C1F5A2714849700922024 /* Scrubbing */ = { isa = PBXGroup; children = ( @@ -5711,6 +5972,7 @@ D23039A6298D513D001A1FA3 /* DatadogInternal */ = { isa = PBXGroup; children = ( + D227A0A22C76229400C83324 /* Benchmarks */, 6167E6DF2B81203A00C3CA2D /* Models */, D23039CA298D5235001A1FA3 /* Attributes */, D23039C3298D5235001A1FA3 /* Codable */, @@ -5719,6 +5981,7 @@ D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */, D23039BD298D5235001A1FA3 /* DatadogFeature.swift */, D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */, + 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */, D23039AD298D5234001A1FA3 /* DD.swift */, D23039D6298D5235001A1FA3 /* Extensions */, D23039BF298D5235001A1FA3 /* MessageBus */, @@ -5748,6 +6011,7 @@ D23039B2298D5235001A1FA3 /* Context */ = { isa = PBXGroup; children = ( + E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */, D23039B3298D5235001A1FA3 /* AppState.swift */, D23039B4298D5235001A1FA3 /* UserInfo.swift */, D23039B5298D5235001A1FA3 /* BatteryStatus.swift */, @@ -5824,6 +6088,7 @@ D23039D6298D5235001A1FA3 /* Extensions */ = { isa = PBXGroup; children = ( + 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */, D23039D8298D5235001A1FA3 /* DatadogExtended.swift */, D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */, D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */, @@ -5944,6 +6209,7 @@ D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */, D24C9C5129A7BD12002057CF /* SamplerMock.swift */, D24C9C5429A7C5F3002057CF /* DateProvider.swift */, + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */, D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */, D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */, 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, @@ -6036,6 +6302,7 @@ D263BCB129DB014900FA0E21 /* Extensions */ = { isa = PBXGroup; children = ( + 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */, D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */, D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */, ); @@ -6154,7 +6421,7 @@ D29D5A4A273BF81500A687C1 /* UIKit */ = { isa = PBXGroup; children = ( - 6141015A251A601D00E3C2D9 /* UIKitRUMUserActionsHandler.swift */, + 6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */, 6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */, F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */, ); @@ -6173,24 +6440,18 @@ isa = PBXGroup; children = ( 61133C0C2423983800786299 /* Logs+objc.swift */, + F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */, ); path = Logs; sourceTree = ""; }; - D2A434A72A8E3FFB0028E329 /* SessionReplay */ = { - isa = PBXGroup; - children = ( - D2A434A82A8E402B0028E329 /* SessionReplay+objc.swift */, - ); - path = SessionReplay; - sourceTree = ""; - }; D2A783D329A53049003B03BB /* Utils */ = { isa = PBXGroup; children = ( D23039D9298D5235001A1FA3 /* DateFormatting.swift */, 613C6B8F2768FDDE00870CBF /* Sampler.swift */, D23039DC298D5235001A1FA3 /* DDError.swift */, + E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */, 61133BBA2423979B00786299 /* SwiftExtensions.swift */, D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */, ); @@ -6200,6 +6461,9 @@ D2A783D629A530B4003B03BB /* Utils */ = { isa = PBXGroup; children = ( + B3C27A072CE6342C006580F9 /* DeterministicSamplerTests.swift */, + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */, + 116F84052CFDD06700705755 /* SampleRateTests.swift */, 613C6B912768FF3100870CBF /* SamplerTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, ); @@ -6240,6 +6504,23 @@ path = Files; sourceTree = ""; }; + D2AD1CC12CE4AE6600106C74 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D2AD1CB92CE4AE6600106C74 /* Color.swift */, + D2AD1CBA2CE4AE6600106C74 /* Color+Reflection.swift */, + D2AD1CBB2CE4AE6600106C74 /* CustomDump.swift */, + D2AD1CBC2CE4AE6600106C74 /* DisplayList.swift */, + D2AD1CBD2CE4AE6600106C74 /* DisplayList+Reflection.swift */, + D2AD1CBE2CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift */, + D2AD1CBF2CE4AE6600106C74 /* Text.swift */, + D2AD1CC02CE4AE6600106C74 /* Text+Reflection.swift */, + 962D72BA2CF6436600F86EF0 /* Image.swift */, + 962D72BB2CF6436600F86EF0 /* Image+Reflection.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; D2C5D5272B83FD3700B63F36 /* Models */ = { isa = PBXGroup; children = ( @@ -6336,6 +6617,7 @@ D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */, D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */, D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */, + D28ABFD52CECDE6B00623F27 /* URLSessionInterceptorTests.swift */, 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */, 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */, 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */, @@ -6360,7 +6642,6 @@ children = ( D2EFA867286DA85700F1FAA6 /* DatadogContextProvider.swift */, D20605A2287464F40047275C /* ContextValuePublisher.swift */, - D20605C62875A77D0047275C /* ContextValueReader.swift */, D20605A5287476230047275C /* ServerOffsetPublisher.swift */, D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */, D20605B12874E1660047275C /* CarrierInfoPublisher.swift */, @@ -7462,6 +7743,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 960B26C02D0360EE00D7196F /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7940,8 +8222,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 */, 614396722A67D74F00197326 /* BatchMetrics.swift in Sources */, @@ -8024,7 +8304,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 */, @@ -8032,12 +8311,13 @@ 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */, 6128F58A2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */, 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 */, D224430D29E95D6700274EC7 /* CrashReportReceiverTests.swift in Sources */, + 96F69D6C2CBE94A800A6178B /* DatadogCoreTests.swift in Sources */, D234613228B7713000055D4C /* FeatureContextTests.swift in Sources */, D21831552B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift in Sources */, 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */, @@ -8052,6 +8332,7 @@ 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, D2A1EE38287EEB7400D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */, D24C9C7129A7D57A002057CF /* DirectoriesMock.swift in Sources */, + F603F1302CAEA7620088E6B7 /* DDInternalLogger+apiTests.m in Sources */, D22743E329DEB90B001A7EF9 /* RUMDebuggingTests.swift in Sources */, 614798992A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */, 61DCC84A2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */, @@ -8082,7 +8363,6 @@ 3C1890152ABDE9BF00CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */, 61133C4B2423990D00786299 /* DDLogsTests.swift in Sources */, - 614B78ED296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */, 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */, D22743E929DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */, 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */, @@ -8130,7 +8410,6 @@ 61F930C52BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, 6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */, 61A2CC242A44454D0000FF25 /* DDRUMTests.swift in Sources */, - D2A1EE26287C35DE00D28DFB /* ContextValueReaderMock.swift in Sources */, D25CFAA329C8644E00E3A43D /* Casting+Tracing.swift in Sources */, 61BAD46A26415FCE001886CA /* OTSpanTests.swift in Sources */, 61B5E42726DFB145000B0A5F /* DDDatadog+apiTests.m in Sources */, @@ -8146,6 +8425,7 @@ D2553807288AA84F00727FAD /* UploadMock.swift in Sources */, D28F836C29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift in Sources */, D22743E629DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */, + F603F12B2CAEA4FA0088E6B7 /* DDInternalLoggerTests.swift in Sources */, D20FD9D62ACC0934004D3569 /* WebLogIntegrationTests.swift in Sources */, D21C26D128A64599005DD405 /* MessageBusTests.swift in Sources */, ); @@ -8155,7 +8435,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 61133C0F2423983800786299 /* ObjcIntercompatibility.swift in Sources */, 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */, 6111C58225C0081F00F5C4A2 /* RUMDataModels+objc.swift in Sources */, 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */, @@ -8163,6 +8442,7 @@ A728ADAB2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */, A79B0F66292BD7CA008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */, 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, + F603F1262CAE9F760088E6B7 /* DDInternalLogger+objc.swift in Sources */, 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */, 3CA852642BF2148200B52CBA /* TraceContextInjection+objc.swift in Sources */, 61133C102423983800786299 /* Logs+objc.swift in Sources */, @@ -8173,8 +8453,8 @@ 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */, 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */, - D2A434AA2A8E40A20028E329 /* SessionReplay+objc.swift in Sources */, 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */, + F6E106542C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */, 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8183,6 +8463,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D2AD1CC22CE4AE6600106C74 /* CustomDump.swift in Sources */, + D2AD1CC32CE4AE6600106C74 /* Color+Reflection.swift in Sources */, + D29C9F692D00739400CD568E /* Reflector.swift in Sources */, + D2AD1CC42CE4AE6600106C74 /* DisplayList+Reflection.swift in Sources */, + D2AD1CC52CE4AE6600106C74 /* DisplayList.swift in Sources */, + D2AD1CC62CE4AE6600106C74 /* Color.swift in Sources */, + D2AD1CC72CE4AE6600106C74 /* Text+Reflection.swift in Sources */, + D2AD1CC82CE4AE6600106C74 /* Text.swift in Sources */, + D2AD1CC92CE4AE6600106C74 /* SwiftUIWireframesBuilder.swift in Sources */, + 969B3B212C33F80500D62400 /* UIActivityIndicatorRecorder.swift in Sources */, 61054EA22A6EE10B00AAA894 /* Scheduler.swift in Sources */, A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */, A7B932FB2B1F6A0A00AE6477 /* EnrichedRecord.swift in Sources */, @@ -8190,6 +8480,8 @@ A7B932FD2B1F6A0A00AE6477 /* EnrichedResource.swift in Sources */, 61054E622A6EE10A00AAA894 /* RecordWriter.swift in Sources */, 61054E692A6EE10A00AAA894 /* UIImage+SessionReplay.swift in Sources */, + D218B0462D072C8400E3F82C /* SessionReplayTelemetry.swift in Sources */, + D2EA0F432C0D941900CB20F8 /* ReflectionMirror.swift in Sources */, 61054E782A6EE10A00AAA894 /* UIDatePickerRecorder.swift in Sources */, 61054E822A6EE10A00AAA894 /* UILabelRecorder.swift in Sources */, A73A54982B16406900E1F7E3 /* ResourcesFeature.swift in Sources */, @@ -8215,13 +8507,15 @@ 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */, 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */, 61054E982A6EE10A00AAA894 /* RecordsBuilder.swift in Sources */, - 61054E9C2A6EE10B00AAA894 /* UIImage+Scaling.swift in Sources */, 61054EA12A6EE10B00AAA894 /* MainThreadScheduler.swift in Sources */, 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */, + 96E414142C2AF56F005A6119 /* UIProgressViewRecorder.swift in Sources */, + 962C41A72CA431370050B747 /* SessionReplayPrivacyOverrides.swift in Sources */, 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */, - 61054E6A2A6EE10A00AAA894 /* UIKitExtensions.swift in Sources */, + 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */, + 962D72BF2CF7538800F86EF0 /* CGImage+SessionReplay.swift in Sources */, + 96F25A832CC7EA4400459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift in Sources */, 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */, 61054E832A6EE10A00AAA894 /* UISwitchRecorder.swift in Sources */, 61054E9A2A6EE10A00AAA894 /* NodesFlattener.swift in Sources */, @@ -8233,14 +8527,18 @@ 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */, 61054E722A6EE10A00AAA894 /* TouchIdentifierGenerator.swift in Sources */, A7B932F52B1F694000AE6477 /* ResourcesProcessor.swift in Sources */, + 962D72BD2CF6436700F86EF0 /* Image+Reflection.swift in Sources */, 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */, + 962D72BC2CF6436700F86EF0 /* Image.swift in Sources */, 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */, - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */, + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */, + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */, 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */, A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */, 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */, 61054E882A6EE10A00AAA894 /* ViewTreeRecordingContext.swift in Sources */, + D2AD1CCC2CE4AE9800106C74 /* UIHostingViewRecorder.swift in Sources */, 61054E932A6EE10A00AAA894 /* MultipartFormData.swift in Sources */, D2BCB2A12B7B8107005C2AAB /* WKWebViewRecorder.swift in Sources */, 61054E712A6EE10A00AAA894 /* TouchSnapshot.swift in Sources */, @@ -8253,8 +8551,10 @@ 61054E7F2A6EE10A00AAA894 /* UISliderRecorder.swift in Sources */, 61054E842A6EE10A00AAA894 /* UITabBarRecorder.swift in Sources */, 61054E682A6EE10A00AAA894 /* PrivacyLevel.swift in Sources */, + D22442C52CA301DA002E71E4 /* UIColor+SessionReplay.swift in Sources */, 61054E8E2A6EE10A00AAA894 /* SRContextPublisher.swift in Sources */, 61054E732A6EE10A00AAA894 /* WindowTouchSnapshotProducer.swift in Sources */, + 96F25A822CC7EA4400459567 /* SessionReplayPrivacyOverrides+objc.swift in Sources */, A70ADCD22B583B1300321BC9 /* UIImageResource.swift in Sources */, 61054E792A6EE10A00AAA894 /* UITextViewRecorder.swift in Sources */, 61054E9B2A6EE10B00AAA894 /* CGRectExtensions.swift in Sources */, @@ -8265,6 +8565,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D28ABFD32CEB87C600623F27 /* UIHostingViewRecorderTests.swift in Sources */, + 969B3B232C33F81E00D62400 /* UIActivityIndicatorRecorderTests.swift in Sources */, + 960B26C32D075BD200D7196F /* DisplayListReflectionTests.swift in Sources */, 61054FD42A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift in Sources */, 61054FB52A6EE1BA00AAA894 /* UISliderRecorderTests.swift in Sources */, 61054FB22A6EE1BA00AAA894 /* UILabelRecorderTests.swift in Sources */, @@ -8274,15 +8577,19 @@ 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 */, + 61054FC32A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift in Sources */, 61054FA82A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift in Sources */, + 96D331ED2CFF740700649EE8 /* GraphicImagePrivacyTests.swift in Sources */, 61054FAD2A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift in Sources */, 61054FD52A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift in Sources */, 61054FC12A6EE1BA00AAA894 /* ViewTreeRecorderTests.swift in Sources */, 61054F982A6EE1BA00AAA894 /* CGRectExtensionsTests.swift in Sources */, D25C834C2B8657CF008E73B1 /* SegmentJSONTests.swift in Sources */, + D2AD1CCF2CE4AEF600106C74 /* ReflectionMirrorTests.swift in Sources */, + 96867B992D08826B004AE0BC /* TextReflectionTests.swift in Sources */, 61054FB42A6EE1BA00AAA894 /* UITabBarRecorderTests.swift in Sources */, 61054FA22A6EE1BA00AAA894 /* TextObfuscatorTests.swift in Sources */, A71013D62B178FAD00101E60 /* ResourcesWriterTests.swift in Sources */, @@ -8293,26 +8600,33 @@ 61054FC22A6EE1BA00AAA894 /* ViewTreeSnapshotTests.swift in Sources */, D2BCB2A32B7B9683005C2AAB /* WKWebViewRecorderTests.swift in Sources */, 61054FC62A6EE1BA00AAA894 /* CoreGraphicsMocks.swift in Sources */, + 96F25A852CC7EB3700459567 /* PrivacyOverridesMock+objc.swift in Sources */, 61054FCA2A6EE1BA00AAA894 /* TestScheduler.swift in Sources */, + 962C41A92CB00FD60050B747 /* DDSessionReplayTests.swift in Sources */, 61054FBD2A6EE1BA00AAA894 /* UIViewRecorderTests.swift in Sources */, 61054F952A6EE1BA00AAA894 /* SessionReplayConfigurationTests.swift in Sources */, - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */, + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */, 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */, + D218B0482D072CF300E3F82C /* SessionReplayTelemetryTests.swift in Sources */, 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */, + D21331C12D132F0600E4A6A1 /* SwiftUIWireframesBuilderTests.swift in Sources */, A74A72872B10CE4100771FEB /* ResourceMocks.swift in Sources */, 61054FA42A6EE1BA00AAA894 /* DiffTests.swift in Sources */, 61054FA02A6EE1BA00AAA894 /* SRCompressionTests.swift in Sources */, A74A72852B10CC6700771FEB /* ResourceRequestBuilderTests.swift in Sources */, - A71265862B17980C007D63CE /* MockFeature.swift in Sources */, 61054FB62A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift in Sources */, 61054F9F2A6EE1BA00AAA894 /* RecordsWriterTests.swift in Sources */, 61054FB82A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift in Sources */, + 962D72C52CF7806300F86EF0 /* GraphicsImageReflectionTests.swift in Sources */, 61054FA32A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift in Sources */, + 96867B9B2D0883DD004AE0BC /* ColorReflectionTests.swift in Sources */, 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */, 61054FC52A6EE1BA00AAA894 /* UIKitMocks.swift in Sources */, 61054FB92A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift in Sources */, 61054FA62A6EE1BA00AAA894 /* SnapshotProcessorTests.swift in Sources */, 61054FB72A6EE1BA00AAA894 /* UISegmentRecorderTests.swift in Sources */, + D29C9F6B2D01D5F600CD568E /* ReflectorTests.swift in Sources */, + 96E863722C9C547B0023BF78 /* SessionReplayOverrideTests.swift in Sources */, A7D9528A2B28BD94004C79B1 /* ResourceProcessorSpy.swift in Sources */, A7D9528C2B28C18D004C79B1 /* ResourceProcessorTests.swift in Sources */, A74A72892B10D95D00771FEB /* MultipartBuilderSpy.swift in Sources */, @@ -8320,17 +8634,20 @@ 61054FC92A6EE1BA00AAA894 /* RecorderMocks.swift in Sources */, 61054FBB2A6EE1BA00AAA894 /* UISwitchRecorderTests.swift in Sources */, A7F651302B7655DE004B0EDB /* UIImageResourceTests.swift in Sources */, - 61054F972A6EE1BA00AAA894 /* UIImage+ScalingTests.swift in Sources */, + 61054F972A6EE1BA00AAA894 /* UIImage+SessionReplayTests.swift in Sources */, 61054FB02A6EE1BA00AAA894 /* ViewTreeSnapshotBuilderTests.swift in Sources */, 61054FD32A6EE1BA00AAA894 /* MultipartFormDataTests.swift in Sources */, 61054FB12A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift in Sources */, 61054F9C2A6EE1BA00AAA894 /* SwiftExtensionsTests.swift in Sources */, + 962D72C72CF7815300F86EF0 /* ReflectionMocks.swift in Sources */, 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */, 61054F9D2A6EE1BA00AAA894 /* MainThreadSchedulerTests.swift in Sources */, - 61054FAA2A6EE1BA00AAA894 /* UIKitExtensionsTests.swift in Sources */, + 61054FAA2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift in Sources */, 61054FA52A6EE1BA00AAA894 /* RecordsBuilderTests.swift in Sources */, 61054FD02A6EE1BA00AAA894 /* SRContextPublisherTests.swift in Sources */, + 962C41A82CA431AA0050B747 /* DDSessionReplayOverridesTests.swift in Sources */, 61054F9B2A6EE1BA00AAA894 /* QueueTests.swift in Sources */, + D2AE9A5D2CF8837C00695264 /* FeatureFlagsMock.swift in Sources */, D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */, 61054F992A6EE1BA00AAA894 /* ColorsTests.swift in Sources */, 61054FBF2A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift in Sources */, @@ -8349,6 +8666,7 @@ 618236892710560900125326 /* DebugWebviewViewController.swift in Sources */, 61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */, 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */, + 3C62C3612C3E852F00C7E336 /* MultiSelector.swift in Sources */, 61E5333824B84EE2003D6C4E /* DebugRUMViewController.swift in Sources */, 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */, 61020C2C2758E853005EEAEA /* DebugBackgroundEventsViewController.swift in Sources */, @@ -8459,7 +8777,9 @@ D207319529A522F600ECBF94 /* LogsFeature.swift in Sources */, D242C29E2A14D6A6004B4980 /* RemoteLogger.swift in Sources */, D20731B529A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, + 615D52BB2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */, D243BBF529A620CC000B9CEC /* MessageReceivers.swift in Sources */, + 615D52B82C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */, D22C5BC92A98A0B30024CC1F /* Baggages.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8470,6 +8790,7 @@ files = ( D2A783E829A53468003B03BB /* ConsoleLoggerTests.swift in Sources */, D2A783EB29A53468003B03BB /* LogSanitizerTests.swift in Sources */, + 615D52BE2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */, D2D30E602A40CD310020C553 /* LogsTests.swift in Sources */, D2A783E729A53468003B03BB /* LogEventBuilderTests.swift in Sources */, D242C2A12A14D747004B4980 /* RemoteLoggerTests.swift in Sources */, @@ -8477,6 +8798,7 @@ D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249972A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C12C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8497,7 +8819,9 @@ D20731AB29A5279D00ECBF94 /* LogsFeature.swift in Sources */, D242C29F2A14D6A7004B4980 /* RemoteLogger.swift in Sources */, D20731B629A528DA00ECBF94 /* LogEventBuilder.swift in Sources */, + 615D52BC2C88A83A00F8B8FC /* SynchronizedTags.swift in Sources */, D243BBF629A620CC000B9CEC /* MessageReceivers.swift in Sources */, + 615D52B92C888C1F00F8B8FC /* SynchronizedAttributes.swift in Sources */, D22C5BC82A98A0B20024CC1F /* Baggages.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8509,6 +8833,7 @@ D23039F9298D5236001A1FA3 /* CoreLogger.swift in Sources */, D2160CA229C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, + E2AA55E72C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */, @@ -8541,6 +8866,7 @@ D21A94F22B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D23039EC298D5236001A1FA3 /* LaunchTime.swift in Sources */, 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */, + D227A0A42C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */, D23039EE298D5236001A1FA3 /* FeatureMessageReceiver.swift in Sources */, D23039DE298D5235001A1FA3 /* Writer.swift in Sources */, D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */, @@ -8554,6 +8880,7 @@ D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D23039E1298D5236001A1FA3 /* AppState.swift in Sources */, D2DE63532A30A7CA00441A54 /* CoreRegistry.swift in Sources */, + E2AA55EA2C32C76A002FEF28 /* WatchKitExtensions.swift in Sources */, D2EBEE2829BA160F00B15732 /* W3CHTTPHeadersWriter.swift in Sources */, D23039EA298D5236001A1FA3 /* DeviceInfo.swift in Sources */, D2EBEE2329BA160F00B15732 /* B3HTTPHeadersReader.swift in Sources */, @@ -8575,8 +8902,10 @@ D23039E8298D5236001A1FA3 /* DatadogContext.swift in Sources */, D23039FF298D5236001A1FA3 /* Foundation+Datadog.swift in Sources */, D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */, + 3CD3A13A2C6C99ED00436A69 /* Data+Crypto.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 +8919,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 +8951,8 @@ D253EE972B988CA90010B589 /* ViewCache.swift in Sources */, D23F8E5A29DDCD28001CFAE8 /* RUMResourceScope.swift in Sources */, D23F8E5C29DDCD28001CFAE8 /* RUMApplicationScope.swift in Sources */, + 3CFF4F982C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, + 61193AAF2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */, D23F8E5D29DDCD28001CFAE8 /* SwiftUIViewModifier.swift in Sources */, D23F8E5E29DDCD28001CFAE8 /* VitalInfo.swift in Sources */, D23F8E5F29DDCD28001CFAE8 /* UIApplicationSwizzler.swift in Sources */, @@ -8630,8 +8962,11 @@ D23F8E6329DDCD28001CFAE8 /* RUMDataModels.swift in Sources */, 61C713AB2A3B790B00FA735A /* Monitor.swift in Sources */, D23F8E6429DDCD28001CFAE8 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, + 3C4CF9952C47CAEA006DE1C0 /* MemoryWarning.swift in Sources */, D23F8E6529DDCD28001CFAE8 /* RUMFeature.swift in Sources */, D23F8E6629DDCD28001CFAE8 /* RUMDebugging.swift in Sources */, + 3C4CF9912C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */, D23F8E6729DDCD28001CFAE8 /* RUMUUID.swift in Sources */, D23F8E6829DDCD28001CFAE8 /* UIKitExtensions.swift in Sources */, 61C713A82A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */, @@ -8643,6 +8978,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 */, @@ -8674,7 +9010,9 @@ D23F8E8229DDCD28001CFAE8 /* RUMSessionScope.swift in Sources */, D23F8E8329DDCD28001CFAE8 /* RUMUser.swift in Sources */, D23F8E8429DDCD28001CFAE8 /* UIKitRUMUserActionsPredicate.swift in Sources */, + 3C5CD8CE2C3ECB9400B12303 /* MemoryWarningReporter.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,7 +9022,8 @@ D23F8E8C29DDCD28001CFAE8 /* RUMBaggageKeys.swift in Sources */, 6174D6212C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */, - D23F8E8E29DDCD28001CFAE8 /* UIKitRUMUserActionsHandler.swift in Sources */, + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, + D23F8E8E29DDCD28001CFAE8 /* UIEventCommandFactory.swift in Sources */, D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */, 61DCC84F2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, ); @@ -8699,23 +9038,27 @@ D23F8EA029DDCD38001CFAE8 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */, 61C4534B2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */, D23F8EA229DDCD38001CFAE8 /* RUMSessionScopeTests.swift in Sources */, + 3C4CF9992C47CC92006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */, D23F8EA329DDCD38001CFAE8 /* RUMUserActionScopeTests.swift in Sources */, 615B0F8C2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */, 61C713B42A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */, D23F8EA529DDCD38001CFAE8 /* UIKitMocks.swift in Sources */, D23F8EA629DDCD38001CFAE8 /* RUMDeviceInfoTests.swift in Sources */, D23F8EA829DDCD38001CFAE8 /* RUMResourceScopeTests.swift in Sources */, - D23F8EA929DDCD38001CFAE8 /* RUMOperatingSystemInfoTests.swift in Sources */, + 3CFF4FA52C0E0FE9006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D23F8EAB29DDCD38001CFAE8 /* RUMDataModelMocks.swift in Sources */, D23F8EAC29DDCD38001CFAE8 /* RUMDataModelsMappingTests.swift in Sources */, 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 */, + 3C4CF99C2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */, + 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */, 6174D61B2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -8725,13 +9068,14 @@ D23F8EB429DDCD38001CFAE8 /* RUMApplicationScopeTests.swift in Sources */, D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */, 61C713CB2A3DC22700FA735A /* RUMTests.swift in Sources */, - D23F8EB829DDCD38001CFAE8 /* UIKitRUMUserActionsHandlerTests.swift in Sources */, + D23F8EB829DDCD38001CFAE8 /* RUMActionsHandlerTests.swift in Sources */, D23F8EB929DDCD38001CFAE8 /* RUMFeatureMocks.swift in Sources */, 61C713AE2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */, D23F8EBA29DDCD38001CFAE8 /* ViewIdentifierTests.swift in Sources */, 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 */, @@ -8765,6 +9109,7 @@ D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */, D2579553298ABB04008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8E2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6929A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7142B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D2579558298ABB04008A1BE5 /* Encoding.swift in Sources */, @@ -8787,6 +9132,7 @@ 3C85D42C29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */, D2160CF729C0EE2B00FAA9A5 /* UploadMocks.swift in Sources */, D2F44FBC299AA36D0074B0D9 /* Decompression.swift in Sources */, + 96F69D6F2CBE94F600A6178B /* MockFeature.swift in Sources */, D24C9C5229A7BD12002057CF /* SamplerMock.swift in Sources */, D2579557298ABB04008A1BE5 /* AttributesMocks.swift in Sources */, D2C9A2872C0F467C007526F5 /* SessionReplayConfigurationMocks.swift in Sources */, @@ -8812,6 +9158,7 @@ D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */, D257957A298ABB83008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8F2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6A29A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7152B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D257957B298ABB83008A1BE5 /* Encoding.swift in Sources */, @@ -8834,6 +9181,7 @@ 3C85D42D29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */, D2160CF829C0EE2B00FAA9A5 /* UploadMocks.swift in Sources */, D2F44FBD299AA36D0074B0D9 /* Decompression.swift in Sources */, + 96F69D6E2CBE94F500A6178B /* MockFeature.swift in Sources */, D24C9C5329A7BD12002057CF /* SamplerMock.swift in Sources */, D257957F298ABB83008A1BE5 /* AttributesMocks.swift in Sources */, D2C9A2882C0F467C007526F5 /* SessionReplayConfigurationMocks.swift in Sources */, @@ -8941,6 +9289,8 @@ D253EE962B988CA90010B589 /* ViewCache.swift in Sources */, D29A9F8429DD85BB005C54A4 /* RUMResourceScope.swift in Sources */, D29A9F7329DD85BB005C54A4 /* RUMApplicationScope.swift in Sources */, + 3CFF4F972C09E64C006F191D /* WatchdogTerminationMonitor.swift in Sources */, + 61193AAE2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */, D29A9F6A29DD85BB005C54A4 /* SwiftUIViewModifier.swift in Sources */, D29A9F6429DD85BB005C54A4 /* VitalInfo.swift in Sources */, D29A9F6D29DD85BB005C54A4 /* UIApplicationSwizzler.swift in Sources */, @@ -8950,8 +9300,11 @@ D29A9F7B29DD85BB005C54A4 /* RUMDataModels.swift in Sources */, 61C713AA2A3B790B00FA735A /* Monitor.swift in Sources */, D29A9F8529DD85BB005C54A4 /* SwiftUIViewHandler.swift in Sources */, + 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, + 3C4CF9942C47CAE9006DE1C0 /* MemoryWarning.swift in Sources */, D29A9F7429DD85BB005C54A4 /* RUMFeature.swift in Sources */, D29A9F7729DD85BB005C54A4 /* RUMDebugging.swift in Sources */, + 3C4CF9922C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */, D29A9F6E29DD85BB005C54A4 /* RUMUUID.swift in Sources */, D29A9F8D29DD8665005C54A4 /* UIKitExtensions.swift in Sources */, 61C713A72A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */, @@ -8963,6 +9316,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 */, @@ -8994,7 +9348,9 @@ D29A9F5C29DD85BB005C54A4 /* RUMSessionScope.swift in Sources */, D29A9F6629DD85BB005C54A4 /* RUMUser.swift in Sources */, D29A9F8229DD85BB005C54A4 /* UIKitRUMUserActionsPredicate.swift in Sources */, + 3C5CD8CD2C3ECB9400B12303 /* MemoryWarningReporter.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,7 +9360,8 @@ D29A9F8329DD85BB005C54A4 /* RUMBaggageKeys.swift in Sources */, 6174D6202C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */, - D29A9F6929DD85BB005C54A4 /* UIKitRUMUserActionsHandler.swift in Sources */, + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, + D29A9F6929DD85BB005C54A4 /* UIEventCommandFactory.swift in Sources */, D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */, 61DCC84E2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, ); @@ -9019,23 +9376,27 @@ D29A9FA629DDB483005C54A4 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */, 61C4534A2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */, D29A9FBD29DDB483005C54A4 /* RUMSessionScopeTests.swift in Sources */, + 3C4CF9982C47CC91006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */, D29A9FAB29DDB483005C54A4 /* RUMUserActionScopeTests.swift in Sources */, 615B0F8B2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */, 61C713B32A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */, D29A9FE029DDC75A005C54A4 /* UIKitMocks.swift in Sources */, D29A9FA329DDB483005C54A4 /* RUMDeviceInfoTests.swift in Sources */, D29A9FBC29DDB483005C54A4 /* RUMResourceScopeTests.swift in Sources */, - D29A9FB029DDB483005C54A4 /* RUMOperatingSystemInfoTests.swift in Sources */, + 3CFF4FA42C0E0FE8006F191D /* WatchdogTerminationCheckerTests.swift in Sources */, D29A9FC629DDBA8A005C54A4 /* RUMDataModelMocks.swift in Sources */, D29A9FD529DDC624005C54A4 /* RUMDataModelsMappingTests.swift in Sources */, 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 */, + 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */, + 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */, @@ -9045,13 +9406,14 @@ D29A9F9F29DDB483005C54A4 /* RUMApplicationScopeTests.swift in Sources */, D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */, 61C713CA2A3DC22700FA735A /* RUMTests.swift in Sources */, - D29A9FAC29DDB483005C54A4 /* UIKitRUMUserActionsHandlerTests.swift in Sources */, + D29A9FAC29DDB483005C54A4 /* RUMActionsHandlerTests.swift in Sources */, D29A9FC029DDB540005C54A4 /* RUMFeatureMocks.swift in Sources */, 61C713AD2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */, D29A9FB729DDB483005C54A4 /* ViewIdentifierTests.swift in Sources */, 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 */, @@ -9067,6 +9429,7 @@ files = ( D2A783F329A534F9003B03BB /* ConsoleLoggerTests.swift in Sources */, D2A783F429A534F9003B03BB /* LogSanitizerTests.swift in Sources */, + 615D52BF2C88A98300F8B8FC /* SynchronizedTagsTests.swift in Sources */, D2D30E612A40CD310020C553 /* LogsTests.swift in Sources */, D2A783F529A534F9003B03BB /* LogEventBuilderTests.swift in Sources */, D242C2A22A14D747004B4980 /* RemoteLoggerTests.swift in Sources */, @@ -9074,6 +9437,7 @@ D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */, D2B249982A45E10500DD4F9F /* LoggerTests.swift in Sources */, D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C22C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -9162,7 +9526,6 @@ D29CDD3328211A2200F7DAA5 /* TLVBlock.swift in Sources */, D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */, A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */, - D20605CA2875A83D0047275C /* ContextValueReader.swift in Sources */, D2A1EE24287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */, D2CB6E2927C50EAE00A62B57 /* KronosInternetAddress.swift in Sources */, 6128F5722BA223D100D35B08 /* DataStore+TLV.swift in Sources */, @@ -9189,7 +9552,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 +9623,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,10 +9630,10 @@ 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 */, - 614B78EE296D7B63009C6B92 /* DatadogCoreTests.swift in Sources */, D2CB6F0C27C520D400A62B57 /* KronosNTPPacketTests.swift in Sources */, D2CB6F0E27C520D400A62B57 /* DDRUMMonitorTests.swift in Sources */, 612C13D12AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */, @@ -9282,6 +9643,7 @@ 6167E7072B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift in Sources */, D2CB6F1327C520D400A62B57 /* DDConfigurationTests.swift in Sources */, D2CB6F1727C520D400A62B57 /* ObjcExceptionHandlerTests.swift in Sources */, + 96F69D6D2CBE94A900A6178B /* DatadogCoreTests.swift in Sources */, D28F836B29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift in Sources */, D2CB6F1827C520D400A62B57 /* DatadogTestsObserver.swift in Sources */, 61F3E36E2BC7D66700C7881E /* HeadBasedSamplingTests.swift in Sources */, @@ -9311,6 +9673,8 @@ 6136CB4B2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */, D25085112976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, A7CA21842BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */, + F603F1312CAEA7630088E6B7 /* DDInternalLogger+apiTests.m in Sources */, + F603F12C2CAEA7180088E6B7 /* DDInternalLoggerTests.swift in Sources */, D2CB6F3327C520D400A62B57 /* FilesOrchestratorTests.swift in Sources */, D2FB1258292E0F10005B13F8 /* TrackingConsentPublisherTests.swift in Sources */, D2CB6F3B27C520D400A62B57 /* NSURLSessionBridge.m in Sources */, @@ -9370,7 +9734,6 @@ D28F836629C9E6A200EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */, 612C13D72AAB35EB0086B5D1 /* SRSegmentMatcher.swift in Sources */, 6147989A2A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */, - D2A1EE27287C35DE00D28DFB /* ContextValueReaderMock.swift in Sources */, D2CB6F7E27C520D400A62B57 /* OTSpanTests.swift in Sources */, D2CB6F7F27C520D400A62B57 /* DDDatadog+apiTests.m in Sources */, D2CB6F8027C520D400A62B57 /* TracingWithLoggingIntegrationTests.swift in Sources */, @@ -9390,8 +9753,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D2CB6F9827C5217A00A62B57 /* ObjcIntercompatibility.swift in Sources */, + F6E106552C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */, D2CB6F9927C5217A00A62B57 /* Casting.swift in Sources */, + F603F1272CAE9F760088E6B7 /* DDInternalLogger+objc.swift in Sources */, D2CB6F9A27C5217A00A62B57 /* RUMDataModels+objc.swift in Sources */, D2CB6F9B27C5217A00A62B57 /* DDSpanContext+objc.swift in Sources */, D2CB6F9C27C5217A00A62B57 /* OTTracer+objc.swift in Sources */, @@ -9457,6 +9821,7 @@ D2DA2358298D57AA00C6C7E6 /* CoreLogger.swift in Sources */, D2160CA329C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, + E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */, @@ -9489,6 +9854,7 @@ D21A94F32B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D2DA2364298D57AA00C6C7E6 /* LaunchTime.swift in Sources */, 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */, + D227A0A52C7622EA00C83324 /* BenchmarkProfiler.swift in Sources */, D2DA2365298D57AA00C6C7E6 /* FeatureMessageReceiver.swift in Sources */, D2DA2366298D57AA00C6C7E6 /* Writer.swift in Sources */, D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */, @@ -9502,6 +9868,7 @@ D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */, D2DE63542A30A7CA00441A54 /* CoreRegistry.swift in Sources */, + E2AA55EC2C32C78B002FEF28 /* WatchKitExtensions.swift in Sources */, D2EBEE3629BA161100B15732 /* W3CHTTPHeadersWriter.swift in Sources */, D2DA236D298D57AA00C6C7E6 /* DeviceInfo.swift in Sources */, D2EBEE3129BA161100B15732 /* B3HTTPHeadersReader.swift in Sources */, @@ -9523,8 +9890,10 @@ D2DA2374298D57AA00C6C7E6 /* DatadogContext.swift in Sources */, D2DA2375298D57AA00C6C7E6 /* Foundation+Datadog.swift in Sources */, D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */, + 3CD3A13B2C6C99ED00436A69 /* Data+Crypto.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 +9907,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 */, @@ -9566,8 +9936,10 @@ 3CCECDAF2BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */, D263BCB429DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, 6174D6162BFDF29B00EC7469 /* BundleTypeTests.swift in Sources */, + 116F84062CFDD06700705755 /* SampleRateTests.swift in Sources */, 3C0D5DF52A5443B100446CF9 /* DataFormatTests.swift in Sources */, D2EBEE4429BA168200B15732 /* TraceIDTests.swift in Sources */, + D28ABFD72CECDE6B00623F27 /* URLSessionInterceptorTests.swift in Sources */, D2EBEE4329BA168200B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23A7298D58F400C6C7E6 /* AppStateHistoryTests.swift in Sources */, D2DA23A5298D58F400C6C7E6 /* AnyDecodableTests.swift in Sources */, @@ -9575,8 +9947,10 @@ D2DA23A4298D58F400C6C7E6 /* AnyCodableTests.swift in Sources */, D2160CDE29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */, D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, + B3C27A082CE6342C006580F9 /* DeterministicSamplerTests.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 */, @@ -9598,6 +9972,7 @@ D2160CD429C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, D263BCB629DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23AA298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, + 3CD3A13D2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */, D2DA23A8298D58F400C6C7E6 /* DeviceInfoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -9615,8 +9990,10 @@ 3CCECDB02BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */, D263BCB529DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */, 6174D6172BFDF29B00EC7469 /* BundleTypeTests.swift in Sources */, + 116F84072CFDD06700705755 /* SampleRateTests.swift in Sources */, 3C0D5DF62A5443B100446CF9 /* DataFormatTests.swift in Sources */, D2EBEE4629BA168400B15732 /* TraceIDTests.swift in Sources */, + D28ABFD62CECDE6B00623F27 /* URLSessionInterceptorTests.swift in Sources */, D2EBEE4529BA168400B15732 /* TraceIDGeneratorTests.swift in Sources */, D2DA23B2298D59DC00C6C7E6 /* AppStateHistoryTests.swift in Sources */, D2DA23B3298D59DC00C6C7E6 /* AnyDecodableTests.swift in Sources */, @@ -9624,8 +10001,10 @@ D2DA23B4298D59DC00C6C7E6 /* AnyCodableTests.swift in Sources */, D2160CDF29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */, D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */, + B3C27A092CE6342C006580F9 /* DeterministicSamplerTests.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 */, @@ -9647,6 +10026,7 @@ D2160CD529C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */, D263BCB729DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */, D2DA23B8298D59DC00C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */, + 3CD3A13C2C6C99FE00436A69 /* Data+CryptoTests.swift in Sources */, D2DA23BA298D59DC00C6C7E6 /* DeviceInfoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -13562,8 +13942,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DataDog/dd-sdk-swift-testing.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.4.0; + kind = exactVersion; + version = "2.5.3-beta1"; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index d2e0f0741f..eaca1e4c15 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -34,20 +34,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - -
@@ -81,92 +67,137 @@ + + + + + + + + + + + + + + + + + + @@ -197,64 +228,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -67,92 +53,137 @@ + + + + + + + + + + + + + + + + + + @@ -183,51 +214,6 @@ ReferencedContainer = "container:Datadog.xcodeproj"> - - - - - - - - - - - - - - - - - - - - @@ -53,27 +53,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme index d1c4d80d64..417294662d 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme @@ -33,9 +33,9 @@ @@ -53,27 +53,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme index b88210c4c2..a4422a9c7e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme index ecd19591ee..251f49560e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme index 8d8c65bf52..96304d7948 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme index c28f7de21f..bb2fb24be0 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme index 6758f81ec1..3749d2bd23 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme @@ -44,98 +44,6 @@ isEnabled = "YES"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme index 1a9cfff2ee..13de3f1975 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme index 440cbbab98..07a31a4557 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme @@ -26,13 +26,15 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> @@ -50,27 +52,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme index 6603bbc485..05898dd9fd 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39,7 +201,7 @@ + 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..0ba9e725e5 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme @@ -26,7 +26,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39,7 +201,7 @@ + Identifier = "OTelSpanTests/testSetActive_givenParentSpan()"> diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme index 977495b8e5..a5fee88576 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme @@ -40,7 +40,169 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Base.lproj/Main iOS.storyboard b/Datadog/Example/Base.lproj/Main iOS.storyboard index fb7c848fb2..f2f1e7d959 100644 --- a/Datadog/Example/Base.lproj/Main iOS.storyboard +++ b/Datadog/Example/Base.lproj/Main iOS.storyboard @@ -3,7 +3,7 @@ - + @@ -1598,7 +1598,7 @@ - + @@ -1621,21 +1621,24 @@ - + + + + - + - + @@ -1706,7 +1709,7 @@ - + @@ -1799,40 +1802,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + - - + + - - - - + @@ -1849,7 +1945,6 @@ - diff --git a/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift index 3a6fcaf15c..1545977968 100644 --- a/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift +++ b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift @@ -6,6 +6,7 @@ import SwiftUI import DatadogTrace +import DatadogInternal @available(iOS 14, *) internal class DebugManualTraceInjectionViewController: UIHostingController { @@ -16,19 +17,33 @@ internal class DebugManualTraceInjectionViewController: UIHostingController String { + switch self { + case .all: + return "All" + case .sampled: + return "Sampled" + } + } +} + @available(iOS 14.0, *) internal struct DebugManualTraceInjectionView: View { - enum TraceHeaderType: String, CaseIterable { + enum TraceHeaderType: String, CaseIterable, Identifiable { case datadog = "Datadog" case w3c = "W3C" case b3Single = "B3-Single" case b3Multiple = "B3-Multiple" + + var id: String { rawValue } } @State private var spanName = "network request" - @State private var requestURL = "http://127.0.0.1:8000" - @State private var selectedTraceHeaderType: TraceHeaderType = .datadog - @State private var sampleRate: Float = 100.0 + @State private var requestURL = "https://httpbin.org/get" + @State private var selectedTraceHeaderTypes: Set = [.datadog, .w3c] + @State private var selectedTraceContextInjection: TraceContextInjection = .all + @State private var sampleRate: SampleRate = .maxSampleRate @State private var isRequestPending = false private let session: URLSession = URLSession( @@ -59,12 +74,18 @@ internal struct DebugManualTraceInjectionView: View { Section(header: Text("Span name:")) { TextField("", text: $spanName) } - Picker("Trace header type:", selection: $selectedTraceHeaderType) { - ForEach(TraceHeaderType.allCases, id: \.self) { headerType in - Text(headerType.rawValue) + Picker("Trace context injection:", selection: $selectedTraceContextInjection) { + ForEach(TraceContextInjection.allCases, id: \.self) { headerType in + Text(headerType.toString()) } } .pickerStyle(.inline) + MultiSelector( + label: Text("Trace header type:"), + options: TraceHeaderType.allCases, + optionToString: { $0.rawValue }, + selected: $selectedTraceHeaderTypes + ) Section(header: Text("Trace sample Rate")) { Slider( value: $sampleRate, @@ -101,42 +122,44 @@ internal struct DebugManualTraceInjectionView: View { } var request = URLRequest(url: url) - request.httpMethod = "POST" + request.httpMethod = "GET" let span = Tracer.shared().startRootSpan(operationName: spanName) - switch selectedTraceHeaderType { - case .datadog: - let writer = HTTPHeadersWriter( - samplingStrategy: .custom(sampleRate: sampleRate), - traceContextInjection: .all - ) - Tracer.shared().inject(spanContext: span.context, writer: writer) - writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } - case .w3c: - let writer = W3CHTTPHeadersWriter( - samplingStrategy: .custom(sampleRate: sampleRate), - tracestate: [:], - traceContextInjection: .all - ) - Tracer.shared().inject(spanContext: span.context, writer: writer) - writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } - case .b3Single: - let writer = B3HTTPHeadersWriter( - samplingStrategy: .custom(sampleRate: sampleRate), - injectEncoding: .single, - traceContextInjection: .all - ) - Tracer.shared().inject(spanContext: span.context, writer: writer) - writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } - case .b3Multiple: - let writer = B3HTTPHeadersWriter( - samplingStrategy: .custom(sampleRate: sampleRate), - injectEncoding: .multiple, - traceContextInjection: .all - ) - Tracer.shared().inject(spanContext: span.context, writer: writer) - writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + for selectedTraceHeaderType in selectedTraceHeaderTypes { + switch selectedTraceHeaderType { + case .datadog: + let writer = HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + traceContextInjection: selectedTraceContextInjection + ) + Tracer.shared().inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + case .w3c: + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + tracestate: [:], + traceContextInjection: selectedTraceContextInjection + ) + Tracer.shared().inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + case .b3Single: + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + injectEncoding: .single, + traceContextInjection: selectedTraceContextInjection + ) + Tracer.shared().inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + case .b3Multiple: + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + injectEncoding: .multiple, + traceContextInjection: selectedTraceContextInjection + ) + Tracer.shared().inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + } } send(request: request) { @@ -147,7 +170,17 @@ internal struct DebugManualTraceInjectionView: View { private func send(request: URLRequest, completion: @escaping () -> Void) { isRequestPending = true - let task = session.dataTask(with: request) { _, _, _ in + let task = session.dataTask(with: request) { data, response, _ in + let httpResponse = response as! HTTPURLResponse + print("🚀 Request completed with status code: \(httpResponse.statusCode)") + + // pretty print response + if let data = data { + let json = try? JSONSerialization.jsonObject(with: data, options: []) + if let json = json { + print("🚀 Response: \(json)") + } + } completion() DispatchQueue.main.async { self.isRequestPending = false } } 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/DebugRUMViewController.swift b/Datadog/Example/Debugging/DebugRUMViewController.swift index 8e70203e3e..dc65bfd189 100644 --- a/Datadog/Example/Debugging/DebugRUMViewController.swift +++ b/Datadog/Example/Debugging/DebugRUMViewController.swift @@ -7,10 +7,10 @@ import UIKit import DatadogRUM import DatadogCore +import DatadogInternal class DebugRUMViewController: UIViewController { @IBOutlet weak var rumServiceNameTextField: UITextField! - @IBOutlet weak var consoleTextView: UITextView! private var simulatedViewControllers: [UIViewController] = [] @@ -18,7 +18,6 @@ class DebugRUMViewController: UIViewController { super.viewDidLoad() rumServiceNameTextField.text = serviceName hideKeyboardWhenTapOutside() - startDisplayingDebugInfo(in: consoleTextView) viewURLTextField.placeholder = viewURL actionViewURLTextField.placeholder = actionViewURL @@ -181,6 +180,51 @@ class DebugRUMViewController: UIViewController { simulatedViewControllers.append(viewController) sendErrorEventButton.disableFor(seconds: 0.5) } + + // MARK: - Telemetry Events + + @IBAction func didTapTelemetryEvent(_ sender: Any) { + guard let button = sender as? UIButton, let title = button.currentTitle else { + return + } + button.disableFor(seconds: 0.5) + + let telemetry = CoreRegistry.default.telemetry + + switch title { + case "debug": + telemetry.debug( + id: UUID().uuidString, + message: "DEBUG telemetry message", + attributes: [ + "attribute-foo": "foo", + "attribute-42": 42, + ] + ) + case "error": + telemetry.error( + id: UUID().uuidString, + message: "ERROR telemetry message", + kind: "error.telemetry.kind", + stack: "error.telemetry.stack" + ) + case "metric": + telemetry.metric( + name: "METRIC telemetry", + attributes: [ + "attribute-foo": "foo", + "attribute-42": 42, + ], + sampleRate: 100 + ) + case "usage": + telemetry.send( + telemetry: .usage(.init(event: .setTrackingConsent(.granted), sampleRate: 100)) + ) + default: + break + } + } } // MARK: - Private Helpers 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..045d08416e 100644 --- a/Datadog/Example/ExampleAppDelegate.swift +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -64,6 +64,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate { // Enable Trace Trace.enable( with: Trace.Configuration( + tags: ["testing-tag": "my-value"], networkInfoEnabled: true, customEndpoint: Environment.readCustomTraceURL() ) @@ -77,8 +78,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/Example/Utils/MultiSelector.swift b/Datadog/Example/Utils/MultiSelector.swift new file mode 100644 index 0000000000..aa2ec5ee9c --- /dev/null +++ b/Datadog/Example/Utils/MultiSelector.swift @@ -0,0 +1,85 @@ +/* + * 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 SwiftUI + +@available(iOS 14.0, *) +struct MultiSelector: View { + let label: LabelView + let options: [Selectable] + let optionToString: (Selectable) -> String + + var selected: Binding> + + private var formattedSelectedListString: String { + ListFormatter.localizedString( + byJoining: selected.wrappedValue.map { + optionToString($0) + } + ) + } + + var body: some View { + NavigationLink(destination: multiSelectionView()) { + HStack { + label + Spacer() + Text(formattedSelectedListString) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + } + } + + private func multiSelectionView() -> some View { + MultiSelectionView( + options: options, + optionToString: optionToString, + selected: selected + ) + } +} + + +@available(iOS 13.0, *) +struct MultiSelectionView: View { + let options: [Selectable] + let optionToString: (Selectable) -> String + + @Binding var selected: Set + + var body: some View { + List { + ForEach(options) { selectable in + Button(action: { + toggleSelection(selectable: selectable) + }) { + HStack { + Text(optionToString(selectable)) + .foregroundColor(.black) + Spacer() + if selected.contains(where: { + $0.id == selectable.id + }) { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + .tag(selectable.id) + } + } + .listStyle(GroupedListStyle()) + } + + private func toggleSelection(selectable: Selectable) { + if let existingIndex = selected.firstIndex(where: { $0.id == selectable.id }) { + selected.remove(at: existingIndex) + } else { + selected.insert(selectable) + } + } +} diff --git a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift index 099d4ea2db..e1789398c6 100644 --- a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift +++ b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift @@ -16,9 +16,11 @@ import DatadogInternal /// - recording crash context data injected from SDK core and features like RUM. private class CrashReporterMock: CrashReportingPlugin { @ReadWriteLock - internal var pendingCrashReport: DDCrashReport? + var pendingCrashReport: DDCrashReport? @ReadWriteLock - internal var injectedContext: Data? = nil + var injectedContext: Data? = nil + /// Custom backtrace reporter injected to the plugin. + var injectedBacktraceReporter: BacktraceReporting? init(pendingCrashReport: DDCrashReport? = nil) { self.pendingCrashReport = pendingCrashReport @@ -26,6 +28,7 @@ private class CrashReporterMock: CrashReportingPlugin { func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) { _ = completion(pendingCrashReport) } func inject(context: Data) { injectedContext = context } + var backtraceReporter: BacktraceReporting? { injectedBacktraceReporter } } /// Covers broad scenarios of sending Crash Reports. @@ -52,6 +55,7 @@ class SendingCrashReportTests: XCTestCase { lastLogAttributes: .init(mockRandomAttributes()) ) let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let crashReportAttributes: [String: Encodable] = try XCTUnwrap(crashReport.additionalAttributes.dd.decode()) // When Logs.enable(with: .init(), in: core) @@ -65,8 +69,8 @@ class SendingCrashReportTests: XCTestCase { XCTAssertEqual(log.error?.message, crashReport.message) XCTAssertEqual(log.error?.kind, crashReport.type) XCTAssertEqual(log.error?.stack, crashReport.stack) - XCTAssertFalse(log.attributes.userAttributes.isEmpty) - DDAssertJSONEqual(log.attributes.userAttributes, crashContext.lastLogAttributes!) + let lastLogAttributes: [String: Encodable] = try XCTUnwrap(crashContext.lastLogAttributes.dd.decode()) + DDAssertJSONEqual(log.attributes.userAttributes, lastLogAttributes.merging(crashReportAttributes) { $1 }) XCTAssertNotNil(log.attributes.internalAttributes?[DDError.threads]) XCTAssertNotNil(log.attributes.internalAttributes?[DDError.binaryImages]) XCTAssertNotNil(log.attributes.internalAttributes?[DDError.meta]) @@ -81,7 +85,9 @@ class SendingCrashReportTests: XCTestCase { XCTAssertNotNil(rumEvent.error.binaryImages) XCTAssertNotNil(rumEvent.error.meta) XCTAssertNotNil(rumEvent.error.wasTruncated) - DDAssertJSONEqual(rumEvent.context!.contextInfo, crashContext.lastRUMAttributes!) + let contextAttributes = try XCTUnwrap(rumEvent.context?.contextInfo) + let lastRUMAttributes = try XCTUnwrap(crashContext.lastRUMAttributes?.attributes) + DDAssertJSONEqual(contextAttributes, lastRUMAttributes.merging(crashReportAttributes) { $1 }) } func testWhenSendingCrashReportAsLog_itIsLinkedToTheRUMSessionThatHasCrashed() throws { diff --git a/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift index 9a72b80ef5..8971d5900c 100644 --- a/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift @@ -25,8 +25,7 @@ class CoreTelemetryIntegrationTests: XCTestCase { func testGivenRUMEnabled_telemetryEventsAreSent() throws { // Given var config = RUM.Configuration(applicationID: .mockAny()) - config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 + config.telemetrySampleRate = .maxSampleRate RUM.enable(with: config, in: core) // When @@ -34,9 +33,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { #sourceLocation(file: "File.swift", line: 42) core.telemetry.error("Error Telemetry") #sourceLocation() - core.telemetry.metric(name: "Metric Name", attributes: ["metric.attribute": 42]) + core.telemetry.metric(name: "Metric Name", attributes: ["metric.attribute": 42], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) // Then @@ -57,7 +57,7 @@ class CoreTelemetryIntegrationTests: XCTestCase { let metric = debugEvents[1] XCTAssertEqual(metric.telemetry.message, "[Mobile Metric] Metric Name") - + let metricAttribute = try XCTUnwrap(metric.telemetry.telemetryInfo["metric.attribute"] as? Int) XCTAssertEqual(metricAttribute, 42) @@ -68,8 +68,7 @@ class CoreTelemetryIntegrationTests: XCTestCase { func testGivenRUMEnabled_whenNoViewIsActive_telemetryEventsAreLinkedToSession() throws { // Given var config = RUM.Configuration(applicationID: "rum-app-id") - config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 + config.telemetrySampleRate = .maxSampleRate RUM.enable(with: config, in: core) // When @@ -79,9 +78,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) @@ -115,8 +115,7 @@ class CoreTelemetryIntegrationTests: XCTestCase { func testGivenRUMEnabled_whenViewIsActive_telemetryEventsAreLinkedToView() throws { // Given var config = RUM.Configuration(applicationID: "rum-app-id") - config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 + config.telemetrySampleRate = .maxSampleRate RUM.enable(with: config, in: core) // When @@ -125,9 +124,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) @@ -161,8 +161,7 @@ class CoreTelemetryIntegrationTests: XCTestCase { func testGivenRUMEnabled_whenActionIsActive_telemetryEventsAreLinkedToAction() throws { // Given var config = RUM.Configuration(applicationID: "rum-app-id") - config.telemetrySampleRate = 100 - config.metricsTelemetrySampleRate = 100 + config.telemetrySampleRate = .maxSampleRate RUM.enable(with: config, in: core) // When @@ -172,9 +171,10 @@ class CoreTelemetryIntegrationTests: XCTestCase { // Then core.telemetry.debug("Debug Telemetry") core.telemetry.error("Error Telemetry") - core.telemetry.metric(name: "Metric Name", attributes: [:]) + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) core.telemetry.stopMethodCalled( - core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom()) + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 ) let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) @@ -204,4 +204,58 @@ class CoreTelemetryIntegrationTests: XCTestCase { XCTAssertNotNil(methodCalledMetric.view?.id) XCTAssertNotNil(methodCalledMetric.action?.id) } + + func testGivenRUMEnabled_effectiveSampleRateIsComposed() throws { + // Given + var config = RUM.Configuration(applicationID: .mockAny()) + config.telemetrySampleRate = 90 + RUM.enable(with: config, in: core) + let metricsSampleRate: SampleRate = 99 + let headSampleRate: SampleRate = 80.0 + + // When + (0..<100).forEach { _ in + core.telemetry.debug("Debug Telemetry") + core.telemetry.error("Error Telemetry") + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: metricsSampleRate) + core.telemetry.send(telemetry: .usage(.init(event: .setUser, sampleRate: metricsSampleRate))) + core.telemetry.stopMethodCalled( + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: headSampleRate), + tailSampleRate: metricsSampleRate + ) + } + + // Then + let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryErrorEvent.self) + let usageEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryUsageEvent.self) + + XCTAssertGreaterThan(debugEvents.count, 0) + XCTAssertGreaterThan(errorEvents.count, 0) + XCTAssertGreaterThan(usageEvents.count, 0) + + let debug = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "Debug Telemetry" })) + XCTAssertEqual(debug.effectiveSampleRate, Double(config.telemetrySampleRate)) + + let error = try XCTUnwrap(errorEvents.first(where: { $0.telemetry.message == "Error Telemetry" })) + XCTAssertEqual(error.effectiveSampleRate, Double(config.telemetrySampleRate)) + + let mobileMetric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Metric Name" })) + XCTAssertEqual( + mobileMetric.effectiveSampleRate, + Double(config.telemetrySampleRate.composed(with: metricsSampleRate)) + ) + + let methodCalledMetric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Method Called" })) + XCTAssertEqual( + methodCalledMetric.effectiveSampleRate, + Double(config.telemetrySampleRate.composed(with: metricsSampleRate).composed(with: headSampleRate)) + ) + + let usage = try XCTUnwrap(usageEvents.first) + XCTAssertEqual( + usage.effectiveSampleRate, + Double(config.telemetrySampleRate.composed(with: metricsSampleRate)) + ) + } } diff --git a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift index 49dca3dcfd..be366661cd 100644 --- a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift @@ -107,12 +107,11 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { applicationID: .mockAny(), urlSessionTracking: .init( resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count + providerDataCount = data?.count ?? 0 providerExpectation.fulfill() return [:] - }) + } + ) ), in: core ) @@ -149,20 +148,18 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { ) let providerExpectation = expectation(description: "provider called") - var providerDataCount = 0 - var providerData: Data? + var providerInfo: (resp: URLResponse?, data: Data?, err: Error?)? + RUM.enable( with: .init( applicationID: .mockAny(), urlSessionTracking: .init( - resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count - data.map { providerData = $0 } + resourceAttributesProvider: { _, resp, data, err in + providerInfo = (resp, data, err) providerExpectation.fulfill() return [:] - }) + } + ) ), in: core ) @@ -182,20 +179,18 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) let taskExpectation = self.expectation(description: "task completed") - var taskDataCount = 0 - var taskData: Data? - let task = session.dataTask(with: request) { data, _, _ in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - taskDataCount = data!.count - data.map { taskData = $0 } + var taskInfo: (resp: URLResponse?, data: Data?, err: Error?)? + + let task = session.dataTask(with: request) { resp, data, err in + taskInfo = (data, resp, err) taskExpectation.fulfill() } task.resume() wait(for: [providerExpectation, taskExpectation], timeout: 10) - XCTAssertEqual(providerDataCount, taskDataCount) - XCTAssertEqual(providerData, taskData) + XCTAssertEqual(providerInfo?.resp, taskInfo?.resp) + XCTAssertEqual(providerInfo?.data, taskInfo?.data) + XCTAssertEqual(providerInfo?.err as? NSError, taskInfo?.err as? NSError) } class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate { diff --git a/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift index c4a67441b2..568bd0cd29 100644 --- a/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift @@ -15,10 +15,8 @@ import DatadogInternal @testable import DatadogWebViewTracking class WebLogIntegrationTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional - private var controller: WKUserContentControllerMock! - // swiftlint:enable implicitly_unwrapped_optional + private var controller: WKUserContentControllerMock! // swiftlint:disable:this implicitly_unwrapped_optional override func setUp() { core = DatadogCoreProxy( @@ -54,7 +52,7 @@ class WebLogIntegrationTests: XCTestCase { { "eventType": "log", "event": { - "date" : \(1635932927012), + "date" : \(1_635_932_927_012), "status": "debug", "message": "message", "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", @@ -72,7 +70,8 @@ class WebLogIntegrationTests: XCTestCase { // Then let logMatcher = try XCTUnwrap(core.waitAndReturnLogMatchers().first) - try logMatcher.assertItFullyMatches(jsonString: """ + try logMatcher.assertItFullyMatches( + jsonString: """ { "date": \(1_635_932_927_012 + 123.toInt64Milliseconds), "ddtags": "version:1.1.1,env:test", @@ -92,7 +91,7 @@ class WebLogIntegrationTests: XCTestCase { // Given let randomApplicationID: String = .mockRandom() let randomUUID: UUID = .mockRandom() - + Logs.enable(in: core) RUM.enable(with: .mockWith(applicationID: randomApplicationID) { $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) @@ -102,7 +101,7 @@ class WebLogIntegrationTests: XCTestCase { { "eventType": "log", "event": { - "date" : \(1635932927012), + "date" : \(1_635_932_927_012), "status": "debug", "message": "message", "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", @@ -122,7 +121,8 @@ class WebLogIntegrationTests: XCTestCase { // Then let expectedUUID = randomUUID.uuidString.lowercased() let logMatcher = try XCTUnwrap(core.waitAndReturnLogMatchers().first) - try logMatcher.assertItFullyMatches(jsonString: """ + try logMatcher.assertItFullyMatches( + jsonString: """ { "date": \(1_635_932_927_012 + 123.toInt64Milliseconds), "ddtags": "version:1.1.1,env:test", diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift index 307ec2fe97..fa5916ec01 100644 --- a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -7,6 +7,7 @@ import XCTest import TestUtilities @testable import DatadogRUM +@testable import DatadogInternal class RUMSessionEndedMetricIntegrationTests: XCTestCase { private let dateProvider = DateProviderMock() @@ -20,8 +21,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { applicationStateHistory: .mockAppInForeground(since: dateProvider.now) ) rumConfig = RUM.Configuration(applicationID: .mockAny()) - rumConfig.telemetrySampleRate = 100 - rumConfig.metricsTelemetrySampleRate = 100 + rumConfig.telemetrySampleRate = .maxSampleRate rumConfig.dateProvider = dateProvider } @@ -37,7 +37,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key", name: "View") // When @@ -52,7 +52,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key1", name: "View1") // When @@ -68,7 +68,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key", name: "View") // When @@ -88,7 +88,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key", name: "View") // When @@ -101,12 +101,12 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { // MARK: - Reporting Session Attributes - func testReportingSessionID() throws { + func testReportingSessionInformation() throws { var currentSessionID: String? RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key", name: "View") monitor.currentSessionID { currentSessionID = $0 } monitor.stopView(key: "key") @@ -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 { @@ -125,7 +126,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() dateProvider.now += 5.seconds monitor.startView(key: "key1", name: "View1") dateProvider.now += 5.seconds @@ -149,7 +150,7 @@ class RUMSessionEndedMetricIntegrationTests: XCTestCase { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() (0..<3).forEach { _ in // Simulate app in foreground: core.context = .mockWith(applicationStateHistory: .mockAppInForeground(since: dateProvider.now)) @@ -180,13 +181,15 @@ 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 { RUM.enable(with: rumConfig, in: core) // Given - let monitor = RUMMonitor.shared(in: core) + let monitor = self.rumMonitor() monitor.startView(key: "key", name: "View") core.flush() @@ -210,6 +213,61 @@ 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 = self.rumMonitor() + 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 = self.rumMonitor() + monitor.startView(key: "key", name: "View") + monitor.stopView(key: "key") // no active view + + // When + (0.. RUMMonitorProtocol { + let monitor = RUMMonitor.shared(in: core) + monitor.dd.scopes.dependencies.sessionEndedMetric.sampleRate = sessionEndedSampleRate + return monitor + } } // MARK: - Helpers @@ -226,4 +284,3 @@ private extension TelemetryDebugEvent { return telemetry.telemetryInfo[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes } } - diff --git a/Datadog/IntegrationUnitTests/RUM/WatchdogTerminationsMonitoringTests.swift b/Datadog/IntegrationUnitTests/RUM/WatchdogTerminationsMonitoringTests.swift new file mode 100644 index 0000000000..aee9bc95ea --- /dev/null +++ b/Datadog/IntegrationUnitTests/RUM/WatchdogTerminationsMonitoringTests.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-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +import DatadogCrashReporting +@testable import DatadogRUM + +class WatchdogTerminationsMonitoringTests: XCTestCase { + var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + var rumConfig = RUM.Configuration(applicationID: .mockAny()) + let device: DeviceInfo = .init( + name: .mockAny(), + model: .mockAny(), + osName: .mockAny(), + osVersion: .mockAny(), + osBuildNumber: .mockAny(), + architecture: .mockAny(), + isSimulator: false, + vendorId: .mockAny(), + isDebugging: false, + systemBootTime: .init() + ) + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + rumConfig.trackWatchdogTerminations = true + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + + super.tearDown() + } + + func testGivenRUMAndCrashReportingEnabled_whenWatchdogTerminatesTheApp_thenWatchdogTerminationEventIsReported() throws { + // given + core.context = .mockWith( + device: device, + trackingConsent: .granted, + applicationStateHistory: .mockAppInForeground() + ) + rumConfig.processID = .mockRandom() + oneOf( + [ // no matter of RUM or CR initialization order + { + RUM.enable(with: self.rumConfig, in: self.core) + CrashReporting.enable(in: self.core) + }, + { + CrashReporting.enable(in: self.core) + RUM.enable(with: self.rumConfig, in: self.core) + }, + ] + ) + + try waitForWatchdogTerminationCheck(core: core) + let monitor = RUMMonitor.shared(in: core) + monitor.startView(key: "foo") + let views = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self) + let errorsBeforeCrash = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + XCTAssertEqual(errorsBeforeCrash.count, 0) + + let erroringView = try XCTUnwrap(views.last) + XCTAssertEqual(erroringView.view.name, "foo") + + core.context = .mockWith( + device: device, + trackingConsent: .pending, + applicationStateHistory: .mockAppInForeground() + ) + + // re-enable RUM to trigger the watchdog termination event + // update the process ID to make sure check treats it as a new app launch + rumConfig.processID = .mockRandom() + oneOf( + [ // no matter of RUM or CR initialization order + { + RUM.enable(with: self.rumConfig, in: self.core) + CrashReporting.enable(in: self.core) + }, + { + CrashReporting.enable(in: self.core) + RUM.enable(with: self.rumConfig, in: self.core) + }, + ] + ) + + try waitForWatchdogTerminationCheck(core: core) + + let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let watchdogCrash = try XCTUnwrap(errors.first) + XCTAssertEqual(watchdogCrash.error.stack, WatchdogTerminationReporter.Constants.stackNotAvailableErrorMessage) + XCTAssertEqual(watchdogCrash.view.name, "foo") + + XCTAssertEqual(watchdogCrash.error.message, WatchdogTerminationReporter.Constants.errorMessage) + XCTAssertEqual(watchdogCrash.error.type, WatchdogTerminationReporter.Constants.errorType) + XCTAssertEqual(watchdogCrash.error.source, .source) + XCTAssertEqual(watchdogCrash.error.category, .watchdogTermination) + } + + /// Watchdog Termination check is done in the background, we need to wait for it to finish before we can proceed with the test + /// - Parameter core: `DatadogCoreProxy` instance + func waitForWatchdogTerminationCheck(core: DatadogCoreProxy) throws { + let watchdogTermination = try XCTUnwrap(core.get(feature: RUMFeature.self)?.instrumentation.watchdogTermination) + while watchdogTermination.currentState != .started { + Thread.sleep(forTimeInterval: .fromMilliseconds(100)) + } + } +} diff --git a/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift index dff5694e51..7b3eb0572d 100644 --- a/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift @@ -14,10 +14,8 @@ import TestUtilities @testable import DatadogWebViewTracking class WebEventIntegrationTests: XCTestCase { - // swiftlint:disable implicitly_unwrapped_optional private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional - private var controller: WKUserContentControllerMock! - // swiftlint:enable implicitly_unwrapped_optional + private var controller: WKUserContentControllerMock! // swiftlint:disable:this implicitly_unwrapped_optional override func setUp() { core = DatadogCoreProxy( @@ -29,7 +27,7 @@ class WebEventIntegrationTests: XCTestCase { ) controller = WKUserContentControllerMock() - + WebViewTracking.enable( tracking: controller, hosts: [], @@ -61,7 +59,7 @@ class WebEventIntegrationTests: XCTestCase { "application": { "id": "xxx" }, - "date": \(1635932927012), + "date": \(1_635_932_927_012), "service": "super", "session": { "id": "0110cab4-7471-480e-aa4e-7ce039ced355", @@ -123,7 +121,8 @@ class WebEventIntegrationTests: XCTestCase { // Then let expectedUUID = randomUUID.uuidString.lowercased() let rumMatcher = try XCTUnwrap(core.waitAndReturnRUMEventMatchers().last) - try rumMatcher.assertItFullyMatches(jsonString: """ + try rumMatcher.assertItFullyMatches( + jsonString: """ { "application": { "id": "\(randomApplicationID)" @@ -235,7 +234,8 @@ class WebEventIntegrationTests: XCTestCase { // Then let expectedUUID = randomUUID.uuidString.lowercased() let rumMatcher = try XCTUnwrap(core.waitAndReturnRUMEventMatchers().last) - try rumMatcher.assertItFullyMatches(jsonString: """ + try rumMatcher.assertItFullyMatches( + jsonString: """ { "type": "telemetry", "date": \(1_712_069_357_432 + 123.toInt64Milliseconds), diff --git a/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift index 80619be8f5..ea830d5cf0 100644 --- a/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift @@ -11,7 +11,8 @@ import WebKit import TestUtilities @testable import DatadogRUM @testable import DatadogWebViewTracking -@_spi(Internal) @testable import DatadogSessionReplay +@_spi(Internal) +@testable import DatadogSessionReplay class WebRecordIntegrationTests: XCTestCase { // swiftlint:disable implicitly_unwrapped_optional @@ -60,7 +61,59 @@ class WebRecordIntegrationTests: XCTestCase { { "eventType": "record", "event": { - "timestamp" : \(1635932927012), + "timestamp" : \(1_635_932_927_012), + "type": 2 + }, + "view": { "id": "\(randomBrowserViewID.uuidString.lowercased())" } + } + """ + + // When + RUMMonitor.shared(in: core).startView(key: "web-view") + controller.send(body: body, from: webView) + controller.flush() + + // Then + let segments = try core.waitAndReturnEventsData(ofFeature: SessionReplayFeature.name) + .map { try SegmentJSON($0, source: .ios) } + let segment = try XCTUnwrap(segments.first) + + let expectedUUID = randomUUID.uuidString.lowercased() + let expectedSlotID = String(webView.hash) + + XCTAssertEqual(segment.applicationID, randomApplicationID) + XCTAssertEqual(segment.sessionID, expectedUUID) + XCTAssertEqual(segment.viewID, randomBrowserViewID.uuidString.lowercased()) + + let record = try XCTUnwrap(segment.records.first) + DDAssertDictionariesEqual(record, [ + "timestamp": 1_635_932_927_012 + 123.toInt64Milliseconds, + "type": 2, + "slotId": expectedSlotID + ]) + } + + func testWebRecordIntegrationWithNewSessionReplayConfigurationAPI() throws { + // Given + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + let randomBrowserViewID: UUID = .mockRandom() + + SessionReplay.enable(with: SessionReplay.Configuration( + replaySampleRate: 100, + textAndInputPrivacyLevel: .mockRandom(), + imagePrivacyLevel: .mockRandom(), + touchPrivacyLevel: .mockRandom() + ), in: core) + RUM.enable(with: .mockWith(applicationID: randomApplicationID) { + $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) + }, in: core) + + let body = """ + { + "eventType": "record", + "event": { + "timestamp" : \(1_635_932_927_012), "type": 2 }, "view": { "id": "\(randomBrowserViewID.uuidString.lowercased())" } diff --git a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift index 7048d9ce06..c9034966ca 100644 --- a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift +++ b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift @@ -38,7 +38,7 @@ class HeadBasedSamplingTests: XCTestCase { client-ios-app: [----- child --------] | all 3: keep or drop client-ios-app: [-- grandchild --] | */ - let localTraceSampling: Float = 50 // keep or drop + let localTraceSampling: SampleRate = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -67,7 +67,7 @@ class HeadBasedSamplingTests: XCTestCase { client-ios-app: [-------- active.span -----] | client-ios-app: [- child1 -][- child2 -] | all 3: keep or drop */ - let localTraceSampling: Float = 50 // keep or drop + let localTraceSampling: SampleRate = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -100,8 +100,8 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] keep */ - let localTraceSampling: Float = 0 // drop all - let distributedTraceSampling: Float = 100 // keep all + let localTraceSampling: SampleRate = 0 // drop all + let distributedTraceSampling: SampleRate = .maxSampleRate // keep all // Given traceConfig.sampleRate = localTraceSampling @@ -139,8 +139,8 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] drop */ - let localTraceSampling: Float = 100 // keep all - let distributedTraceSampling: Float = 0 // drop all + let localTraceSampling: SampleRate = .maxSampleRate // keep all + let distributedTraceSampling: SampleRate = 0 // drop all // Given traceConfig.sampleRate = localTraceSampling @@ -179,8 +179,8 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] keep */ - let localTraceSampling: Float = 100 // keep all - let distributedTraceSampling: Float = 0 // drop all + let localTraceSampling: SampleRate = .maxSampleRate // keep all + let distributedTraceSampling: SampleRate = 0 // drop all // Given traceConfig.sampleRate = localTraceSampling @@ -227,8 +227,8 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] drop */ - let localTraceSampling: Float = 0 // drop all - let distributedTraceSampling: Float = 100 // keep all + let localTraceSampling: SampleRate = 0 // drop all + let distributedTraceSampling: SampleRate = .maxSampleRate // keep all // Given traceConfig.sampleRate = localTraceSampling @@ -275,7 +275,7 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] keep */ - let localTraceSampling: Float = 100 // keep all + let localTraceSampling: SampleRate = .maxSampleRate // keep all // Given traceConfig.sampleRate = localTraceSampling @@ -314,7 +314,7 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] drop */ - let localTraceSampling: Float = 0 // drop all + let localTraceSampling: SampleRate = 0 // drop all // Given traceConfig.sampleRate = localTraceSampling @@ -354,7 +354,7 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] keep */ - let localTraceSampling: Float = 100 // keep all + let localTraceSampling: SampleRate = .maxSampleRate // keep all // Given traceConfig.sampleRate = localTraceSampling @@ -402,7 +402,7 @@ class HeadBasedSamplingTests: XCTestCase { client backend: [--- backend span ---] drop */ - let localTraceSampling: Float = 0 // drop all + let localTraceSampling: SampleRate = 0 // drop all // Given traceConfig.sampleRate = localTraceSampling diff --git a/DatadogAlamofireExtension.podspec b/DatadogAlamofireExtension.podspec index eda30456ee..c725e4ff30 100644 --- a/DatadogAlamofireExtension.podspec +++ b/DatadogAlamofireExtension.podspec @@ -1,7 +1,12 @@ Pod::Spec.new do |s| s.name = "DatadogAlamofireExtension" - s.version = "2.13.0" + s.version = "2.22.0" s.summary = "An Official Extensions of Datadog Swift SDK for Alamofire." + s.description = <<-DESC + The DatadogAlamofireExtension pod is deprecated and will no longer be maintained. + Please refer to the following documentation on how to instrument Alamofire with the Datadog iOS SDK: + https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/integrated_libraries/ios + DESC s.homepage = "https://www.datadoghq.com" s.social_media_url = "https://twitter.com/datadoghq" @@ -14,6 +19,8 @@ Pod::Spec.new do |s| "Maciej Burda" => "maciej.burda@datadoghq.com" } + s.deprecated = true + s.swift_version = '5.9' s.ios.deployment_target = '12.0' s.tvos.deployment_target = '12.0' diff --git a/DatadogCore.podspec b/DatadogCore.podspec index 36bdcc9523..3c66db090d 100644 --- a/DatadogCore.podspec +++ b/DatadogCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "DatadogCore" - s.version = "2.13.0" + s.version = "2.22.0" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" @@ -17,6 +17,7 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.ios.deployment_target = '12.0' s.tvos.deployment_target = '12.0' + s.watchos.deployment_target = '7.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } diff --git a/DatadogCore/Private/ObjcAppLaunchHandler.m b/DatadogCore/Private/ObjcAppLaunchHandler.m index 3239d61391..bea30ced2a 100644 --- a/DatadogCore/Private/ObjcAppLaunchHandler.m +++ b/DatadogCore/Private/ObjcAppLaunchHandler.m @@ -9,8 +9,10 @@ #import "ObjcAppLaunchHandler.h" -#if TARGET_OS_IOS || TARGET_OS_TV +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION #import +#elif TARGET_OS_OSX +#import #endif // A very long application launch time is most-likely the result of a pre-warmed process. @@ -42,9 +44,17 @@ + (void)load { // This is called at the `DatadogPrivate` load time, keep the work minimal _shared = [[self alloc] initWithProcessInfo:NSProcessInfo.processInfo loadTime:CFAbsoluteTimeGetCurrent()]; -#if TARGET_OS_IOS || TARGET_OS_TV + + NSString *notificationName; +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION + notificationName = UIApplicationDidBecomeActiveNotification; +#elif TARGET_OS_OSX + notificationName = NSApplicationDidBecomeActiveNotification; +#endif + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST || TARGET_OS_VISION || TARGET_OS_OSX NSNotificationCenter * __weak center = NSNotificationCenter.defaultCenter; - id __block __unused token = [center addObserverForName:UIApplicationDidBecomeActiveNotification + id __block __unused token = [center addObserverForName:notificationName object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *_){ diff --git a/DatadogCore/Private/ObjcExceptionHandler.m b/DatadogCore/Private/ObjcExceptionHandler.m index e246b7358c..d287fdcf2b 100644 --- a/DatadogCore/Private/ObjcExceptionHandler.m +++ b/DatadogCore/Private/ObjcExceptionHandler.m @@ -9,7 +9,7 @@ @implementation __dd_private_ObjcExceptionHandler -- (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error { ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error { @try { tryBlock(); return YES; diff --git a/DatadogCore/Private/include/ObjcExceptionHandler.h b/DatadogCore/Private/include/ObjcExceptionHandler.h index bfe9c68972..ed1c57e981 100644 --- a/DatadogCore/Private/include/ObjcExceptionHandler.h +++ b/DatadogCore/Private/include/ObjcExceptionHandler.h @@ -10,8 +10,8 @@ NS_ASSUME_NONNULL_BEGIN @interface __dd_private_ObjcExceptionHandler : NSObject -- (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error - NS_SWIFT_NAME(rethrowToSwift(tryBlock:)); ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error + NS_SWIFT_NAME(rethrow(_:)); @end diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift index f6c86d18b5..34f5530e6b 100644 --- a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -7,14 +7,13 @@ #if canImport(UIKit) import UIKit import DatadogInternal +#if canImport(WatchKit) +import WatchKit +#endif internal final class ApplicationStatePublisher: ContextValuePublisher { typealias Snapshot = AppStateHistory.Snapshot - private static var currentApplicationState: UIApplication.State { - UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state - } - /// The default publisher queue. private static let defaultQueue = DispatchQueue( label: "com.datadoghq.app-state-publisher", @@ -51,19 +50,21 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// Creates a Application state publisher for publishing application state /// history. /// + /// **Note**: It must be called on the main thread. + /// /// - Parameters: - /// - initialState: The initial application state. - /// - queue: The queue for publishing the history. - /// - dateProvider: The date provider for the Application state snapshot timestamp. + /// - appStateProvider: The provider to access the current application state. /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. + /// - dateProvider: The date provider for the Application state snapshot timestamp. + /// - queue: The queue for publishing the history. init( - initialState: AppState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default + appStateProvider: AppStateProvider, + notificationCenter: NotificationCenter, + dateProvider: DateProvider, + queue: DispatchQueue = ApplicationStatePublisher.defaultQueue ) { let initialValue = AppStateHistory( - initialState: initialState, + initialState: appStateProvider.current, date: dateProvider.now ) @@ -74,36 +75,12 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { self.notificationCenter = notificationCenter } - /// Creates a Application state publisher for publishing application state - /// history. - /// - /// **Note**: It must be called on the main thread. - /// - /// - Parameters: - /// - applicationState: The current shared `UIApplication` state. - /// - queue: The queue for publishing the history. - /// - dateProvider: The date provider for the Application state snapshot timestamp. - /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. - convenience init( - applicationState: UIApplication.State = ApplicationStatePublisher.currentApplicationState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default - ) { - self.init( - initialState: AppState(applicationState), - queue: queue, - dateProvider: dateProvider, - notificationCenter: notificationCenter - ) - } - func publish(to receiver: @escaping ContextValueReceiver) { queue.async { self.receiver = receiver } - notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - notificationCenter.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) - notificationCenter.addObserver(self, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - notificationCenter.addObserver(self, selector: #selector(applicationWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive), name: ApplicationNotifications.didBecomeActive, object: nil) + notificationCenter.addObserver(self, selector: #selector(applicationWillResignActive), name: ApplicationNotifications.willResignActive, object: nil) + notificationCenter.addObserver(self, selector: #selector(applicationDidEnterBackground), name: ApplicationNotifications.didEnterBackground, object: nil) + notificationCenter.addObserver(self, selector: #selector(applicationWillEnterForeground), name: ApplicationNotifications.willEnterForeground, object: nil) } @objc @@ -135,10 +112,10 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { } func cancel() { - notificationCenter.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - notificationCenter.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) - notificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) - notificationCenter.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + notificationCenter.removeObserver(self, name: ApplicationNotifications.didBecomeActive, object: nil) + notificationCenter.removeObserver(self, name: ApplicationNotifications.willResignActive, object: nil) + notificationCenter.removeObserver(self, name: ApplicationNotifications.didEnterBackground, object: nil) + notificationCenter.removeObserver(self, name: ApplicationNotifications.willEnterForeground, object: nil) queue.async { self.receiver = nil } } } diff --git a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift index db33ee23b5..1b1c529ff3 100644 --- a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift +++ b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift @@ -24,11 +24,11 @@ internal final class BatteryStatusPublisher: ContextValuePublisher { /// Creates a battery status publisher from the given device. /// /// - Parameters: - /// - device: The `UIDevice` instance. `.current` by default. /// - notificationCenter: The notification center for observing the `UIDevice` battery changes, + /// - device: The `UIDevice` instance. init( - device: UIDevice = .current, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + device: UIDevice ) { self.device = device self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/Context/ContextValueReader.swift b/DatadogCore/Sources/Core/Context/ContextValueReader.swift deleted file mode 100644 index e64dc0546e..0000000000 --- a/DatadogCore/Sources/Core/Context/ContextValueReader.swift +++ /dev/null @@ -1,120 +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 - -/// Defines a read closure for mutating value. -private typealias ContextValueMutation = (inout Value) -> Void - -/// Declares that a type can read values on demand. -/// -/// A reader delivers elements to the receiver callback synchronously. -/// The receiver's ``ContextValueReceiver/Value`` generic type must match the -/// ``ContextValueReader/Value`` types declared by the reader. -/// -/// The reader implements the ``ContextValuePublisher/read(_:)`` method -/// to read the value and call the receiver. -internal protocol ContextValueReader { - /// The kind of values this reader reads. - associatedtype Value - - /// Reads the value synchronously and mutate the value. - /// - /// - Parameter receiver: The value to mutate on read. - func read(to receiver: inout Value) -} - -// MARK: - Type-Erasure - -/// A reader that performs type erasure by wrapping another reader. -/// -/// ``AnyContextValueReader`` is a concrete implementation of ``ContextValueReader`` -/// that has no significant properties of its own, and passes through value to its upstream -/// reader. -/// -/// Use ``AnyContextValueReader`` to wrap a reader whose type has details -/// you don’t want to expose across API boundaries, such as different modules -/// -/// You can use extension method ``ContextValueReader/eraseToAnyReader()`` -/// operator to wrap a publisher with ``ContextValueReader``. -internal struct AnyContextValueReader: ContextValueReader { - private var mutation: ContextValueMutation - - /// Creates a type-erasing reader to wrap the provided reader. - /// - /// - Parameter reader: A reader to wrap with a type-eraser. - init(_ reader: Reader) where Reader: ContextValueReader, Reader.Value == Value { - self.mutation = reader.read - } - - /// Reads the value synchronously and mutate the value. - /// - /// - Parameter receiver: The value to mutate on read. - func read(to receiver: inout Value) { - mutation(&receiver) - } -} - -extension ContextValueReader { - /// Wraps this reader with a type eraser. - /// - /// Use ``ContextValueReader/eraseToAnyReader()`` to expose an instance of - /// ``AnyContextValueReader`` to the downstream subscriber, rather than this reader’s - /// actual type. This form of _type erasure_ preserves abstraction across API boundaries. - /// - /// - Returns: An ``AnyContextValueReader`` wrapping this reader. - func eraseToAnyReader() -> AnyContextValueReader { - return AnyContextValueReader(self) - } -} - -// MARK: - Key-path Reader - -/// A reader that performs key-path mutations by wrapping other readers. -/// -/// ``KeyPathContextValueReader`` keeps an array of mutation operations by calling -/// ``ContextValueReader/read`` to write to a value's property at given ``WritableKeyPath``. -/// -/// Use ``KeyPathContextValueReader`` to wrap readers to mutate properties of -/// a value. -internal struct KeyPathContextValueReader { - private var mutations: [ContextValueMutation] = [] - - /// Appends a ``ContextValueReader`` instance to set the value's property at a given - /// `keyPath`. - /// - /// - Parameters: - /// - reader: The reader to append. - /// - keyPath: The value's writable `keyPath`. - mutating func append(reader: Reader, receiver keyPath: WritableKeyPath) where Reader: ContextValueReader { - mutations.append { value in - reader.read(to: &value[keyPath: keyPath]) - } - } - - /// Reads the value synchronously and mutate the value. - /// - /// - Parameter receiver: The value to mutate on read. - func read(to receiver: inout Value) { - mutations.forEach { mutation in - mutation(&receiver) - } - } -} - -// MARK: - No-op - -/// A no-operation reader. -/// -/// ``NOPContextValueReader`` is a concrete implementation of ``ContextValueReader`` -/// that has no effect when invoking ``ContextValueReader/read``. -/// -/// You can use ``NOPContextValueReader`` as a placeholder. -internal struct NOPContextValueReader: ContextValueReader { - func read(to receiver: inout Value) { - // no-op - } -} diff --git a/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift index b328816b44..61980e0cf0 100644 --- a/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift +++ b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift @@ -40,9 +40,7 @@ internal final class DatadogContextProvider { /// The current `context`. /// /// The value must be accessed from the `queue` only. - private var context: DatadogContext { - didSet { receivers.forEach { $0(context) } } - } + private var context: DatadogContext /// The queue used to synchronize the access to the `DatadogContext`. internal let queue = DispatchQueue( @@ -56,9 +54,6 @@ internal final class DatadogContextProvider { /// List of subscription of context values. private var subscriptions: [ContextValueSubscription] - /// A reader for key-path values of the context. - private var reader: KeyPathContextValueReader - /// Creates a context provider to perform reads and writes on the /// shared Datadog context. /// @@ -67,21 +62,12 @@ internal final class DatadogContextProvider { self.context = context self.receivers = [] self.subscriptions = [] - self.reader = KeyPathContextValueReader() } deinit { subscriptions.forEach { $0.cancel() } } - /// Reads current context. - /// - /// **Warning:** Must be called from the `queue`. - private func unsafeRead() -> DatadogContext { - reader.read(to: &self.context) - return context - } - /// Publishes context changes to the given receiver. /// /// - Parameter receiver: The receiver closure. @@ -96,21 +82,26 @@ internal final class DatadogContextProvider { /// /// - Returns: The current context. func read() -> DatadogContext { - queue.sync(execute: unsafeRead) + queue.sync { context } } /// Reads to the `context` asynchronously, without blocking the caller thread. /// /// - Parameter block: The block closure called with the current context. func read(block: @escaping (DatadogContext) -> Void) { - queue.async { block(self.unsafeRead()) } + queue.async { block(self.context) } } /// Writes to the `context` asynchronously, without blocking the caller thread. /// /// - Parameter block: The block closure called with the current context. func write(block: @escaping (inout DatadogContext) -> Void) { - queue.async { block(&self.context) } + queue.async { + block(&self.context) + self.receivers.forEach { receiver in + receiver(self.context) + } + } } /// Subscribes a context's property to a publisher. @@ -135,23 +126,6 @@ internal final class DatadogContextProvider { } } - /// Assigns a value reader to a context property. - /// - /// The context provider has the ability to a assign a value reader that complies to - /// ``ContextValueReader`` to a specific context property. e.g.: - /// - /// let reader = ServerOffsetReader() - /// provider.assign(reader: reader, to: \.serverTimeOffset) - /// - /// - Parameters: - /// - reader: The value reader. - /// - keyPath: A context's key path that supports reading from and writing to the resulting value. - func assign(reader: Reader, to keyPath: WritableKeyPath) where Reader: ContextValueReader { - queue.async { - self.reader.append(reader: reader, receiver: keyPath) - } - } - #if DD_SDK_COMPILED_FOR_TESTING func replace(context newContext: DatadogContext) { queue.async { diff --git a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift index 11168138a3..7e314439fe 100644 --- a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift +++ b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift @@ -19,11 +19,11 @@ internal final class LowPowerModePublisher: ContextValuePublisher { /// Creates a low power mode publisher. /// /// - Parameters: - /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. /// - notificationCenter: The notification center for observing the `NSProcessInfoPowerStateDidChange`, + /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. init( - processInfo: ProcessInfo = .processInfo, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + processInfo: ProcessInfo ) { self.initialValue = processInfo.isLowPowerModeEnabled self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift index c5aa6d348f..750dda7166 100644 --- a/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift +++ b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift @@ -9,7 +9,7 @@ import DatadogInternal /// A concrete implementation of the `DataStore` protocol using file storage. internal final class FeatureDataStore: DataStore { - private enum Constants { + enum Constants { /// The version of this data store implementation. /// If a breaking change is introduced to the format of managed files, the version must be upgraded and old data should be deleted. static let dataStoreVersion = 1 @@ -35,7 +35,7 @@ internal final class FeatureDataStore: DataStore { ) { self.feature = feature self.coreDirectory = directory - self.directoryPath = "\(Constants.dataStoreVersion)/" + feature + self.directoryPath = coreDirectory.getDataStorePath(forFeatureNamed: feature) self.queue = queue self.telemetry = telemetry } @@ -87,6 +87,18 @@ internal final class FeatureDataStore: DataStore { } } + func clearAllData() { + queue.async { + do { + let directory = try self.coreDirectory.coreDirectory.subdirectory(path: self.directoryPath) + try directory.deleteAllFiles() + } catch let error { + DD.logger.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: error) + self.telemetry.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: DDError(error: error)) + } + } + } + // MARK: - Persistence private func write(data: Data, forKey key: String, version: DataStoreKeyVersion) throws { diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 266035e300..189f7280b4 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -53,10 +53,7 @@ internal final class DatadogCore { /// Registry for Features. @ReadWriteLock - private(set) var stores: [String: ( - storage: FeatureStorage, - upload: FeatureUpload - )] = [:] + private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:] /// Registry for Features. @ReadWriteLock @@ -126,7 +123,7 @@ internal final class DatadogCore { /// Sets current user information. /// /// Those will be added to logs, traces and RUM events automatically. - /// + /// /// - Parameters: /// - id: User ID, if any /// - name: Name representing the user, if any @@ -159,11 +156,16 @@ internal final class DatadogCore { } /// Sets the tracking consent regarding the data collection for the Datadog SDK. - /// + /// /// - Parameter trackingConsent: new consent value, which will be applied for all data collected from now on func set(trackingConsent: TrackingConsent) { if trackingConsent != consentPublisher.consent { - allStorages.forEach { $0.migrateUnauthorizedData(toConsent: trackingConsent) } + contextProvider.queue.async { [allStorages] in + // RUM-3175: To prevent race conditions with ongoing "event write" operations, + // data migration must be synchronized on the context queue. This guarantees that + // all latest events have been written before migration occurs. + allStorages.forEach { $0.migrateUnauthorizedData(toConsent: trackingConsent) } + } consentPublisher.consent = trackingConsent } } @@ -171,6 +173,7 @@ internal final class DatadogCore { /// Clears all data that has not already yet been uploaded Datadog servers. func clearAllData() { allStorages.forEach { $0.clearAllData() } + allDataStores.forEach { $0.clearAllData() } } /// Adds a message receiver to the bus. @@ -197,6 +200,13 @@ internal final class DatadogCore { stores.values.map { $0.upload } } + private var allDataStores: [DataStore] { + features.values.compactMap { feature in + let featureType = type(of: feature) as DatadogFeature.Type + return scope(for: featureType).dataStore + } + } + /// Awaits completion of all asynchronous operations, forces uploads (without retrying) and deinitializes /// this instance of the SDK. It **blocks the caller thread**. /// @@ -250,6 +260,7 @@ extension DatadogCore: DatadogCoreProtocol { dateProvider: dateProvider, performance: performancePreset, encryption: encryption, + backgroundTasksEnabled: backgroundTasksEnabled, telemetry: telemetry ) @@ -308,7 +319,7 @@ extension DatadogCore: DatadogCoreProtocol { } } -internal class CoreFeatureScope: FeatureScope where Feature: DatadogFeature { +internal class CoreFeatureScope: @unchecked Sendable, FeatureScope where Feature: DatadogFeature { private weak var core: DatadogCore? private let store: FeatureDataStore @@ -394,8 +405,11 @@ extension DatadogContextProvider { applicationVersion: String, sdkInitDate: Date, device: DeviceInfo, + processInfo: ProcessInfo, dateProvider: DateProvider, - serverDateProvider: ServerDateProvider + serverDateProvider: ServerDateProvider, + notificationCenter: NotificationCenter, + appStateProvider: AppStateProvider ) { let context = DatadogContext( site: site, @@ -436,14 +450,18 @@ extension DatadogContextProvider { #endif #if os(iOS) && !targetEnvironment(simulator) - subscribe(\.batteryStatus, to: BatteryStatusPublisher()) - subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher()) + subscribe(\.batteryStatus, to: BatteryStatusPublisher(notificationCenter: notificationCenter, device: .current)) + subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher(notificationCenter: notificationCenter, processInfo: processInfo)) #endif #if os(iOS) || os(tvOS) DispatchQueue.main.async { // must be call on the main thread to read `UIApplication.State` - let applicationStatePublisher = ApplicationStatePublisher(dateProvider: dateProvider) + let applicationStatePublisher = ApplicationStatePublisher( + appStateProvider: appStateProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider + ) self.subscribe(\.applicationStateHistory, to: applicationStatePublisher) } #endif @@ -460,6 +478,9 @@ extension DatadogCore: Flushable { // The order of flushing below must be considered cautiously and // follow our design choices around SDK core's threading. + // Reset baggages that need not be persisted across flushes. + set(baggage: nil, forKey: LaunchReport.baggageKey) + let features = features.values.compactMap { $0 as? Flushable } // The flushing is repeated few times, to make sure that operations spawned from other operations @@ -485,3 +506,23 @@ extension DatadogCore: Flushable { } } } + +extension DatadogCore: Storage { + /// Returns the most recent modification date of a file in the core directory. + /// - 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 mostRecentModifiedFileAt(before: Date) throws -> 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/MessageBus.swift b/DatadogCore/Sources/Core/MessageBus.swift index cd446ccbee..c264236fb6 100644 --- a/DatadogCore/Sources/Core/MessageBus.swift +++ b/DatadogCore/Sources/Core/MessageBus.swift @@ -72,7 +72,7 @@ internal final class MessageBus { } /// Removes the given key and its associated receiver from the bus. - /// + /// /// - Parameter key: The key to remove along with its associated receiver. func removeReceiver(forKey key: String) { queue.async { self.bus.removeValue(forKey: key) } diff --git a/DatadogCore/Sources/Core/Storage/Directories.swift b/DatadogCore/Sources/Core/Storage/Directories.swift index 22282515cb..6954f189c4 100644 --- a/DatadogCore/Sources/Core/Storage/Directories.swift +++ b/DatadogCore/Sources/Core/Storage/Directories.swift @@ -35,11 +35,22 @@ internal struct CoreDirectory { authorized: try coreDirectory.createSubdirectory(path: "\(name)/v2") ) } + + /// Obtains the path to the data store for given Feature. + /// + /// Note: `FeatureDataStore` directory is created on-demand which may happen before `FeatureDirectories` are created. + /// Hence, this method only returns the path and let the caller decide if the directory should be created. + /// + /// - Parameter name: The given Feature name. + /// - Returns: The path to the data store for given Feature. + func getDataStorePath(forFeatureNamed name: String) -> String { + return "\(FeatureDataStore.Constants.dataStoreVersion)/" + name + } } internal extension CoreDirectory { /// Creates the core directory. - /// + /// /// - Parameters: /// - osDirectory: the root OS directory (`/Library/Caches`) to create core directory inside. /// - instanceName: The core instance name. diff --git a/DatadogCore/Sources/Core/Storage/FeatureStorage.swift b/DatadogCore/Sources/Core/Storage/FeatureStorage.swift index ee30072024..31eb3f13d5 100644 --- a/DatadogCore/Sources/Core/Storage/FeatureStorage.swift +++ b/DatadogCore/Sources/Core/Storage/FeatureStorage.swift @@ -116,6 +116,7 @@ extension FeatureStorage { dateProvider: DateProvider, performance: PerformancePreset, encryption: DataEncryption?, + backgroundTasksEnabled: Bool, telemetry: Telemetry ) { let trackName = BatchMetric.trackValue(for: featureName) @@ -133,7 +134,8 @@ extension FeatureStorage { return FilesOrchestrator.MetricsData( trackName: trackName, consentLabel: BatchMetric.consentGrantedValue, - uploaderPerformance: performance + uploaderPerformance: performance, + backgroundTasksEnabled: backgroundTasksEnabled ) } ) @@ -146,7 +148,8 @@ extension FeatureStorage { return FilesOrchestrator.MetricsData( trackName: trackName, consentLabel: BatchMetric.consentPendingValue, - uploaderPerformance: performance + uploaderPerformance: performance, + backgroundTasksEnabled: backgroundTasksEnabled ) } ) diff --git a/DatadogCore/Sources/Core/Storage/Files/Directory.swift b/DatadogCore/Sources/Core/Storage/Files/Directory.swift index 76fdfd239b..dabb9f61f0 100644 --- a/DatadogCore/Sources/Core/Storage/Files/Directory.swift +++ b/DatadogCore/Sources/Core/Storage/Files/Directory.swift @@ -7,8 +7,19 @@ import Foundation import DatadogInternal +extension Data { + static let empty = Data() +} + +/// 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 +32,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 { @@ -49,9 +110,7 @@ internal struct Directory { /// Creates file with given name. func createFile(named fileName: String) throws -> File { let fileURL = url.appendingPathComponent(fileName, isDirectory: false) - guard FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: nil) == true else { - throw InternalError(description: "Cannot create file at path: \(fileURL.path)") - } + try Data.empty.write(to: fileURL, options: .atomic) return File(url: fileURL) } diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift index 0dce3b8f2a..e4ad03dfda 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, Equatable { 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..282a462524 100644 --- a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -15,10 +15,16 @@ 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. internal class FilesOrchestrator: FilesOrchestratorType { + enum Constants { + /// Precision in which the timestamp is stored as part of the file name. + static let fileNamePrecision: TimeInterval = 0.001 // millisecond precision + } + /// Directory where files are stored. let directory: Directory /// Date provider. @@ -47,11 +53,17 @@ internal class FilesOrchestrator: FilesOrchestratorType { let consentLabel: String /// The preset for uploader performance in this feature to include in metric. let uploaderPerformance: UploadPerformancePreset + /// The present configuration of background upload in this feature to include in metric. + let backgroundTasksEnabled: Bool } /// 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, @@ -102,7 +114,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { // happens too often). try purgeFilesDirectoryIfNeeded() - let newFileName = fileNameFrom(fileCreationDate: dateProvider.now) + let newFileName = nextFileName() let newFile = try directory.createFile(named: newFileName) lastWritableFileName = newFile.name lastWritableFileObjectsCount = 1 @@ -111,6 +123,27 @@ internal class FilesOrchestrator: FilesOrchestratorType { return newFile } + /// Generates a unique file name based on the current time, ensuring that the generated file name does not already exist in the directory. + /// When a conflict is detected, it adjusts the timestamp by advancing the current time by the precision interval to ensure uniqueness. + /// + /// In practice, name conflicts are extremely unlikely due to the monotonic nature of `dateProvider.now`. + /// Conflicts can only occur in very specific scenarios, such as during a tracking consent change when files are moved + /// from an unauthorized (.pending) folder to an authorized (.granted) folder, with events being written immediately before + /// and after the consent change. These conflicts were observed in tests, causing flakiness. In real-device scenarios, + /// conflicts may occur if tracking consent is changed and two events are written within the precision window defined + /// by `Constants.fileNamePrecision` (1 millisecond). + private func nextFileName() -> String { + var newFileName = fileNameFrom(fileCreationDate: dateProvider.now) + while directory.hasFile(named: newFileName) { + // Advance the timestamp by the precision interval to avoid generating the same file name. + // This may result in generating file names "in the future", but we aren't concerned + // about this given how rare this scenario is. + let newDate = dateProvider.now.addingTimeInterval(Constants.fileNamePrecision) + newFileName = fileNameFrom(fileCreationDate: newDate) + } + return newFileName + } + private func reuseLastWritableFileIfPossible(writeSize: UInt64) -> WritableFile? { if let lastFileName = lastWritableFileName { if !directory.hasFile(named: lastFileName) { @@ -244,8 +277,10 @@ internal class FilesOrchestrator: FilesOrchestratorType { BatchDeletedMetric.uploaderWindowKey: performance.uploaderWindow.toMilliseconds, BatchDeletedMetric.batchAgeKey: batchAge.toMilliseconds, BatchDeletedMetric.batchRemovalReasonKey: deletionReason.toString(), - BatchDeletedMetric.inBackgroundKey: false - ] + BatchDeletedMetric.inBackgroundKey: false, + BatchDeletedMetric.backgroundTasksEnabled: metricsData.backgroundTasksEnabled + ], + sampleRate: BatchDeletedMetric.sampleRate ) } @@ -271,7 +306,8 @@ internal class FilesOrchestrator: FilesOrchestratorType { BatchClosedMetric.batchSizeKey: lastWritableFileApproximatedSize, BatchClosedMetric.batchEventsCountKey: lastWritableFileObjectsCount, BatchClosedMetric.batchDurationKey: batchDuration.toMilliseconds - ] + ], + sampleRate: BatchClosedMetric.sampleRate ) } } @@ -279,7 +315,7 @@ internal class FilesOrchestrator: FilesOrchestratorType { /// File creation date is used as file name - timestamp in milliseconds is used for date representation. /// This function converts file creation date into file name. internal func fileNameFrom(fileCreationDate: Date) -> String { - let milliseconds = fileCreationDate.timeIntervalSinceReferenceDate * 1_000 + let milliseconds = fileCreationDate.timeIntervalSinceReferenceDate / FilesOrchestrator.Constants.fileNamePrecision let converted = (try? UInt64(withReportingOverflow: milliseconds)) ?? 0 return String(converted) } @@ -287,6 +323,6 @@ internal func fileNameFrom(fileCreationDate: Date) -> String { /// File creation date is used as file name - timestamp in milliseconds is used for date representation. /// This function converts file name into file creation date. internal func fileCreationDateFrom(fileName: String) -> Date { - let millisecondsSinceReferenceDate = TimeInterval(UInt64(fileName) ?? 0) / 1_000 + let millisecondsSinceReferenceDate = TimeInterval(UInt64(fileName) ?? 0) * FilesOrchestrator.Constants.fileNamePrecision return Date(timeIntervalSinceReferenceDate: TimeInterval(millisecondsSinceReferenceDate)) } 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/Core/Upload/BackgroundTaskCoordinator.swift b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift index c6c417a9df..5ddba5f8b7 100644 --- a/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift +++ b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift @@ -20,6 +20,7 @@ internal protocol BackgroundTaskCoordinator { import UIKit import DatadogInternal +#if !os(watchOS) /// Bridge protocol that calls corresponding `UIApplication` interface for background tasks. Allows easier testablity. internal protocol UIKitAppBackgroundTaskCoordinator { func beginBgTask(_ handler: (() -> Void)?) -> UIBackgroundTaskIdentifier @@ -69,6 +70,7 @@ internal class AppBackgroundTaskCoordinator: BackgroundTaskCoordinator { self.currentTaskId = nil } } +#endif /// Bridge protocol that matches `ProcessInfo` interface for background activity. Allows easier testablity. internal protocol ProcessInfoActivityCoordinator { diff --git a/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift index 9fec6f414c..9e76b72ba3 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift @@ -28,12 +28,17 @@ internal struct DataUploadConditions { } func blockersForUpload(with context: DatadogContext) -> [Blocker] { + var blockers: [Blocker] = [] + #if !os(watchOS) guard let reachability = context.networkConnectionInfo?.reachability else { // when `NetworkConnectionInfo` is not yet available return [.networkReachability(description: "unknown")] } let networkIsReachable = reachability == .yes || reachability == .maybe - var blockers: [Blocker] = networkIsReachable ? [] : [.networkReachability(description: reachability.rawValue)] + if !networkIsReachable { + blockers = [.networkReachability(description: reachability.rawValue)] + } + #endif guard let battery = context.batteryStatus, battery.state != .unknown else { // Note: in RUMS-132 we got the report on `.unknown` battery state reporing `-1` battery level on iPad device diff --git a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift index 333ef7d36c..e1a00db198 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift @@ -72,28 +72,32 @@ internal struct DataUploadStatus { let userDebugDescription: String let error: DataUploadError? + + let attempt: UInt } extension DataUploadStatus { // MARK: - Initialization - init(httpResponse: HTTPURLResponse, ddRequestID: String?) { + init(httpResponse: HTTPURLResponse, ddRequestID: String?, attempt: UInt) { let statusCode = HTTPResponseStatusCode(rawValue: httpResponse.statusCode) ?? .unexpected self.init( needsRetry: statusCode.needsRetry, responseCode: httpResponse.statusCode, - userDebugDescription: "[response code: \(httpResponse.statusCode) (\(statusCode)), request ID: \(ddRequestID ?? "(???)")]", - error: DataUploadError(status: httpResponse.statusCode) + userDebugDescription: "[response code: \(httpResponse.statusCode) (\(statusCode)), request ID: \(ddRequestID ?? "(???)")", + error: DataUploadError(status: httpResponse.statusCode), + attempt: attempt ) } - init(networkError: Error) { + init(networkError: Error, attempt: UInt) { self.init( needsRetry: true, // retry this upload as it failed due to network transport isse responseCode: nil, userDebugDescription: "[error: \(DDError(error: networkError).message)]", // e.g. "[error: A data connection is not currently allowed]" - error: DataUploadError(networkError: networkError) + error: DataUploadError(networkError: networkError), + attempt: attempt ) } } diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift index b2d6aba5bc..6e8f722ebd 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -44,6 +44,8 @@ internal class DataUploadWorker: DataUploadWorkerType { /// Background task coordinator responsible for registering and ending background tasks for UIKit targets. private var backgroundTaskCoordinator: BackgroundTaskCoordinator? + private var previousUploadStatus: DataUploadStatus? + init( queue: DispatchQueue, fileReader: Reader, @@ -113,8 +115,11 @@ internal class DataUploadWorker: DataUploadWorkerType { do { let uploadStatus = try self.dataUploader.upload( events: batch.events, - context: context + context: context, + previous: previousUploadStatus ) + previousUploadStatus = uploadStatus + if uploadStatus.needsRetry { DD.logger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") self.delay.increase() @@ -129,6 +134,7 @@ internal class DataUploadWorker: DataUploadWorkerType { batch, reason: .intakeCode(responseCode: uploadStatus.responseCode) ) + previousUploadStatus = nil } if let error = uploadStatus.error { @@ -144,6 +150,7 @@ internal class DataUploadWorker: DataUploadWorkerType { } catch let error { // If upload can't be initiated do not retry, so drop the batch: self.fileReader.markBatchAsRead(batch, reason: .invalid) + previousUploadStatus = nil self.telemetry.error("Failed to initiate '\(self.featureName)' data upload", error: error) } } @@ -173,12 +180,21 @@ internal class DataUploadWorker: DataUploadWorkerType { // metrics or telemetry. This is legitimate as long as `flush()` routine is only available for testing // purposes and never run in production apps. self.fileReader.markBatchAsRead(nextBatch, reason: .flushed) + previousUploadStatus = nil } do { // Try uploading the batch and do one more retry on failure. - _ = try self.dataUploader.upload(events: nextBatch.events, context: self.contextProvider.read()) + previousUploadStatus = try self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) } catch { - _ = try? self.dataUploader.upload(events: nextBatch.events, context: self.contextProvider.read()) + previousUploadStatus = try? self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) } } } diff --git a/DatadogCore/Sources/Core/Upload/DataUploader.swift b/DatadogCore/Sources/Core/Upload/DataUploader.swift index 3178a0923e..0c89d211fc 100644 --- a/DatadogCore/Sources/Core/Upload/DataUploader.swift +++ b/DatadogCore/Sources/Core/Upload/DataUploader.swift @@ -6,10 +6,11 @@ import Foundation import DatadogInternal +import CommonCrypto /// A type that performs data uploads. internal protocol DataUploaderType { - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus + func upload(events: [Event], context: DatadogContext, previous: DataUploadStatus?) throws -> DataUploadStatus } /// Synchronously uploads data to server using `HTTPClient`. @@ -19,7 +20,8 @@ internal final class DataUploader: DataUploaderType { needsRetry: false, responseCode: nil, userDebugDescription: "", - error: nil + error: nil, + attempt: 0 ) private let httpClient: HTTPClient @@ -32,8 +34,17 @@ internal final class DataUploader: DataUploaderType { /// Uploads data synchronously (will block current thread) and returns the upload status. /// Uses timeout configured for `HTTPClient`. - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { - let request = try requestBuilder.request(for: events, with: context) + func upload(events: [Event], context: DatadogContext, previous: DataUploadStatus?) throws -> DataUploadStatus { + let attempt: UInt + if let previous = previous { + attempt = previous.attempt + 1 + } else { + attempt = 0 + } + + let execution: ExecutionContext = .init(previousResponseCode: previous?.responseCode, attempt: attempt) + let request = try requestBuilder.request(for: events, with: context, execution: execution) + let requestID = request.value(forHTTPHeaderField: URLRequestBuilder.HTTPHeader.ddRequestIDHeaderField) var uploadStatus: DataUploadStatus? @@ -43,9 +54,16 @@ internal final class DataUploader: DataUploaderType { httpClient.send(request: request) { result in switch result { case .success(let httpResponse): - uploadStatus = DataUploadStatus(httpResponse: httpResponse, ddRequestID: requestID) + uploadStatus = DataUploadStatus( + httpResponse: httpResponse, + ddRequestID: requestID, + attempt: attempt + ) case .failure(let error): - uploadStatus = DataUploadStatus(networkError: error) + uploadStatus = DataUploadStatus( + networkError: error, + attempt: attempt + ) } semaphore.signal() diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift index dcdd25d1a8..5d6f4304cc 100644 --- a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift @@ -38,7 +38,11 @@ internal struct FeatureUpload { let backgroundTaskCoordinator: BackgroundTaskCoordinator? switch (backgroundTasksEnabled, isRunFromExtension) { case (true, false): + #if os(watchOS) + backgroundTaskCoordinator = ExtensionBackgroundTaskCoordinator() + #else backgroundTaskCoordinator = AppBackgroundTaskCoordinator() + #endif case (true, true): backgroundTaskCoordinator = ExtensionBackgroundTaskCoordinator() case (false, _): diff --git a/DatadogCore/Sources/Core/Upload/URLSessionClient.swift b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift index 1cf66dd5d9..0523a44af9 100644 --- a/DatadogCore/Sources/Core/Upload/URLSessionClient.swift +++ b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift @@ -18,6 +18,7 @@ internal class URLSessionClient: HTTPClient { configuration.urlCache = nil configuration.connectionProxyDictionary = proxyConfiguration + #if !os(watchOS) // URLSession does not set the `Proxy-Authorization` header automatically when using a proxy // configuration. We manually set the HTTP basic authentication header. if let user = proxyConfiguration?[kCFProxyUsernameKey] as? String, @@ -25,6 +26,7 @@ internal class URLSessionClient: HTTPClient { let authorization = basicHTTPAuthentication(username: user, password: password) configuration.httpAdditionalHeaders = ["Proxy-Authorization": authorization] } + #endif self.init(session: URLSession(configuration: configuration)) } diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 1b06cbf415..a731ff8a09 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -226,6 +226,12 @@ public enum Datadog { internal var httpClientFactory: ([AnyHashable: Any]?) -> HTTPClient = { proxyConfiguration in URLSessionClient(proxyConfiguration: proxyConfiguration) } + + /// The default notification center used for subscribing to app lifecycle events and system notifications. + internal var notificationCenter: NotificationCenter = .default + + /// The default application state provider for accessing [application state](https://developer.apple.com/documentation/uikit/uiapplication/state). + internal var appStateProvider: AppStateProvider = DefaultAppStateProvider() } /// Verbosity level of Datadog SDK. Can be used for debugging purposes. @@ -401,6 +407,91 @@ public enum Datadog { throw ProgrammerError(description: "The '\(instanceName)' instance of SDK is already initialized.") } + registerObjcExceptionHandlerOnce() + + try isValid(clientToken: configuration.clientToken) + try isValid(env: configuration.env) + + let core = try DatadogCore( + configuration: configuration, + trackingConsent: trackingConsent, + instanceName: instanceName + ) + + CITestIntegration.active?.startIntegration() + + CoreRegistry.register(core, named: instanceName) + deleteV1Folders(in: core) + + DD.logger = InternalLogger( + dateProvider: configuration.dateProvider, + timeZone: .current, + printFunction: consolePrint, + verbosityLevel: { Datadog.verbosityLevel } + ) + + return core + } + + private static func deleteV1Folders(in core: DatadogCore) { + let deprecated = ["com.datadoghq.logs", "com.datadoghq.traces", "com.datadoghq.rum"].compactMap { + try? Directory.cache().subdirectory(path: $0) // ignore errors - deprecated paths likely do not exist + } + + core.readWriteQueue.async { + // ignore errors + deprecated.forEach { try? FileManager.default.removeItem(at: $0.url) } + } + } + + /// Flushes all authorised data for each feature, tears down and deinitializes the SDK. + /// - It flushes all data authorised for each feature by performing its arbitrary upload (without retrying). + /// - It completes all pending asynchronous work in each feature. + /// + /// This is highly experimental API and only supported in tests. +#if DD_SDK_COMPILED_FOR_TESTING + public static func flushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { + internalFlushAndDeinitialize(instanceName: instanceName) + } +#endif + + internal static func internalFlushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { + // Unregister core instance: + let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore + // Flush and tear down SDK core: + core?.flushAndTearDown() + } +} + +private func isValid(env: String) throws { + /// 1. cannot be more than 200 chars (including `env:` prefix) + /// 2. cannot end with `:` + /// 3. can contain letters, numbers and _:./-_ (other chars are converted to _ at backend) + let regex = #"^[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]$"# + if env.range(of: regex, options: .regularExpression, range: nil, locale: nil) == nil { + throw ProgrammerError(description: "`env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)") + } +} + +private func isValid(clientToken: String) throws { + if clientToken.isEmpty { + throw ProgrammerError(description: "`clientToken` cannot be empty.") + } +} + +extension DatadogCore { + /// The primary entry point for creating a `DatadogCore` instance. + /// + /// - Parameters: + /// - configuration: A configuration object that encapsulates both user-defined options and internal dependencies + /// passed to SDK's downstream components. + /// - trackingConsent: The user's consent regarding data tracking for the SDK. + /// - instanceName: A unique name for this SDK instance. + convenience init( + configuration: Datadog.Configuration, + trackingConsent: TrackingConsent, + instanceName: String + ) throws { let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) if debug { consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) @@ -432,8 +523,7 @@ public enum Datadog { ) let isRunFromExtension = bundleType == .iOSAppExtension - // Set default `DatadogCore`: - let core = DatadogCore( + self.init( directory: try CoreDirectory( in: configuration.systemDirectory(), instanceName: instanceName, @@ -446,9 +536,9 @@ public enum Datadog { encryption: configuration.encryption, contextProvider: DatadogContextProvider( site: configuration.site, - clientToken: try ifValid(clientToken: configuration.clientToken), + clientToken: configuration.clientToken, service: service, - env: try ifValid(env: configuration.env), + env: configuration.env, version: applicationVersion, buildNumber: applicationBuildNumber, buildId: buildId, @@ -462,9 +552,12 @@ public enum Datadog { applicationBundleType: bundleType, applicationVersion: applicationVersion, sdkInitDate: configuration.dateProvider.now, - device: DeviceInfo(), + device: DeviceInfo(processInfo: configuration.processInfo), + processInfo: configuration.processInfo, dateProvider: configuration.dateProvider, - serverDateProvider: configuration.serverDateProvider + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter, + appStateProvider: configuration.appStateProvider ), applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, @@ -472,7 +565,7 @@ public enum Datadog { isRunFromExtension: isRunFromExtension ) - core.telemetry.configuration( + telemetry.configuration( backgroundTasksEnabled: configuration.backgroundTasksEnabled, batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), batchSize: performance.uploaderWindow.toInt64Milliseconds, @@ -480,66 +573,5 @@ public enum Datadog { useLocalEncryption: configuration.encryption != nil, useProxy: configuration.proxyConfiguration != nil ) - - CITestIntegration.active?.startIntegration() - - CoreRegistry.register(core, named: instanceName) - deleteV1Folders(in: core) - - DD.logger = InternalLogger( - dateProvider: configuration.dateProvider, - timeZone: .current, - printFunction: consolePrint, - verbosityLevel: { Datadog.verbosityLevel } - ) - - return core - } - - private static func deleteV1Folders(in core: DatadogCore) { - let deprecated = ["com.datadoghq.logs", "com.datadoghq.traces", "com.datadoghq.rum"].compactMap { - try? Directory.cache().subdirectory(path: $0) // ignore errors - deprecated paths likely do not exist - } - - core.readWriteQueue.async { - // ignore errors - deprecated.forEach { try? FileManager.default.removeItem(at: $0.url) } - } - } - - /// Flushes all authorised data for each feature, tears down and deinitializes the SDK. - /// - It flushes all data authorised for each feature by performing its arbitrary upload (without retrying). - /// - It completes all pending asynchronous work in each feature. - /// - /// This is highly experimental API and only supported in tests. -#if DD_SDK_COMPILED_FOR_TESTING - public static func flushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { - internalFlushAndDeinitialize(instanceName: instanceName) - } -#endif - - internal static func internalFlushAndDeinitialize(instanceName: String = CoreRegistry.defaultInstanceName) { - // Unregister core instance: - let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore - // Flush and tear down SDK core: - core?.flushAndTearDown() - } -} - -private func ifValid(env: String) throws -> String { - /// 1. cannot be more than 200 chars (including `env:` prefix) - /// 2. cannot end with `:` - /// 3. can contain letters, numbers and _:./-_ (other chars are converted to _ at backend) - let regex = #"^[a-zA-Z0-9_:./-]{0,195}[a-zA-Z0-9_./-]$"# - if env.range(of: regex, options: .regularExpression, range: nil, locale: nil) == nil { - throw ProgrammerError(description: "`env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)") - } - return env -} - -private func ifValid(clientToken: String) throws -> String { - if clientToken.isEmpty { - throw ProgrammerError(description: "`clientToken` cannot be empty.") } - return clientToken } diff --git a/DatadogCore/Sources/Kronos/KronosDNSResolver.swift b/DatadogCore/Sources/Kronos/KronosDNSResolver.swift index 4d8ca1e40f..5b6dfe9819 100644 --- a/DatadogCore/Sources/Kronos/KronosDNSResolver.swift +++ b/DatadogCore/Sources/Kronos/KronosDNSResolver.swift @@ -30,6 +30,9 @@ internal final class KronosDNSResolver { timeout: TimeInterval = kDefaultTimeout, completion: @escaping ([KronosInternetAddress]) -> Void ) { + #if os(watchOS) + completion([]) + #else let callback: CFHostClientCallBack = { host, _, _, info in guard let info = info else { return @@ -79,8 +82,10 @@ internal final class KronosDNSResolver { CFHostSetClient(hostReference, callback, &clientContext) CFHostScheduleWithRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) CFHostStartInfoResolution(hostReference, .addresses, nil) + #endif } + #if !os(watchOS) @objc private func onTimeout() { defer { @@ -99,4 +104,5 @@ internal final class KronosDNSResolver { CFHostUnscheduleFromRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) CFHostSetClient(hostReference, nil, nil) } + #endif } diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift index 1ebd818528..b7deb4ce21 100644 --- a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -38,6 +38,9 @@ internal enum BatchDeletedMetric { static let name = "Batch Deleted" /// Metric type value. static let typeValue = "batch deleted" + /// The sample rate for this metric. + /// It is applied in addition to the telemetry sample rate (20% by default). + static let sampleRate: Float = 1.5 // 1.5% /// The key for uploader's delay options. static let uploaderDelayKey = "uploader_delay" /// The min delay of uploads for this track (in ms). @@ -53,6 +56,8 @@ internal enum BatchDeletedMetric { static let batchRemovalReasonKey = "batch_removal_reason" /// If the batch was deleted in the background. static let inBackgroundKey = "in_background" + /// If the background tasks were enabled. + static let backgroundTasksEnabled = "background_tasks_enabled" /// Allowed values for `batchRemovalReasonKey`. enum RemovalReason { @@ -107,6 +112,9 @@ internal enum BatchClosedMetric { static let name = "Batch Closed" /// Metric type value. static let typeValue = "batch closed" + /// The sample rate for this metric. + /// It is applied in addition to the telemetry sample rate (20% by default). + static let sampleRate: Float = 1.5 // 1.5% /// The default duration since last write (in ms) after which the uploader considers the file to be "ready for upload". static let uploaderWindowKey = "uploader_window" diff --git a/DatadogCore/Sources/Versioning.swift b/DatadogCore/Sources/Versioning.swift index 238c6822ab..c8b72a2e4e 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.22.0" diff --git a/DatadogCore/Tests/Datadog/Core/FeatureTests.swift b/DatadogCore/Tests/Datadog/Core/FeatureTests.swift index b659feb597..8e7b3e7c71 100644 --- a/DatadogCore/Tests/Datadog/Core/FeatureTests.swift +++ b/DatadogCore/Tests/Datadog/Core/FeatureTests.swift @@ -23,6 +23,7 @@ class FeatureStorageTests: XCTestCase { dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), performance: .mockRandom(), encryption: nil, + backgroundTasksEnabled: .mockRandom(), telemetry: NOPTelemetry() ) temporaryFeatureDirectories.create() 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/FilesOrchestrator+MetricsTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift index 789f042cce..d6a8d88b46 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -38,7 +38,8 @@ class FilesOrchestrator_MetricsTests: XCTestCase { metricsData: FilesOrchestrator.MetricsData( trackName: "track name", consentLabel: "consent value", - uploaderPerformance: upload + uploaderPerformance: upload, + backgroundTasksEnabled: .mockAny() ) ) } @@ -68,9 +69,11 @@ class FilesOrchestrator_MetricsTests: XCTestCase { ], "uploader_window": storage.uploaderWindow.toMilliseconds, "in_background": false, + "background_tasks_enabled": false, "batch_age": expectedBatchAge.toMilliseconds, "batch_removal_reason": "intake-code-202", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } func testWhenObsoleteFileIsDeleted_itSendsBatchDeletedMetric() throws { @@ -97,9 +100,11 @@ class FilesOrchestrator_MetricsTests: XCTestCase { ], "uploader_window": storage.uploaderWindow.toMilliseconds, "in_background": false, + "background_tasks_enabled": false, "batch_age": (storage.maxFileAgeForRead + 1).toMilliseconds, "batch_removal_reason": "obsolete", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } func testWhenDirectoryIsPurged_itSendsBatchDeletedMetrics() throws { @@ -129,9 +134,11 @@ class FilesOrchestrator_MetricsTests: XCTestCase { ], "uploader_window": storage.uploaderWindow.toMilliseconds, "in_background": false, + "background_tasks_enabled": false, "batch_age": expectedBatchAge.toMilliseconds, "batch_removal_reason": "purged", ]) + XCTAssertEqual(metric.sampleRate, BatchDeletedMetric.sampleRate) } // MARK: - "Batch Closed" Metric @@ -170,5 +177,6 @@ class FilesOrchestrator_MetricsTests: XCTestCase { "batch_events_count": expectedWrites.count, "batch_duration": expectedWriteDelays.reduce(0, +).toMilliseconds ]) + XCTAssertEqual(metric.sampleRate, BatchClosedMetric.sampleRate) } } diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift index 35ce6dc27d..cb781d6f97 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -169,6 +169,31 @@ class FilesOrchestratorTests: XCTestCase { XCTAssertNil(try? orchestrator.directory.file(named: file2.name)) } + func testWhenFileAlreadyExists_itWaitsAndCreatesFileWithNextName() throws { + let date: Date = .mockDecember15th2019At10AMUTC() + let dateProvider = RelativeDateProvider( + startingFrom: date, + advancingBySeconds: FilesOrchestrator.Constants.fileNamePrecision + ) + + // Given: A file with the current time already exists + let orchestrator = configureOrchestrator(using: dateProvider) + let existingFile = try orchestrator.directory.createFile(named: fileNameFrom(fileCreationDate: date)) + + // When: The orchestrator attempts to create a new file with the next available name + let nextFile = try orchestrator.getWritableFile(writeSize: 1) + + // Then + let existingFileDate = fileCreationDateFrom(fileName: existingFile.name) + let nextFileDate = fileCreationDateFrom(fileName: nextFile.name) + XCTAssertNotEqual(existingFile.name, nextFile.name, "The new file should have a different name than the existing file") + XCTAssertGreaterThanOrEqual( + nextFileDate.timeIntervalSince(existingFileDate), + FilesOrchestrator.Constants.fileNamePrecision, + "The timestamp of the new file should be at least `fileNamePrecision` later than the existing file" + ) + } + // MARK: - Readable file tests func testGivenNoReadableFiles_whenObtainingFiles_itReturnsEmpty() { diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift index aa3c8f0216..6c6dacf7a5 100644 --- a/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift @@ -149,7 +149,13 @@ 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, + backgroundTasksEnabled: .mockAny() + ) ), encryption: nil, telemetry: NOPTelemetry() @@ -168,7 +174,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 +187,13 @@ class FileWriterTests: XCTestCase { directory: directory, performance: PerformancePreset.mockAny(), dateProvider: SystemDateProvider(), - telemetry: NOPTelemetry() + telemetry: NOPTelemetry(), + metricsData: .init( + trackName: "rum", + consentLabel: .mockAny(), + uploaderPerformance: UploadPerformanceMock.noOp, + backgroundTasksEnabled: .mockAny() + ) ), encryption: nil, telemetry: NOPTelemetry() @@ -189,7 +201,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 +214,13 @@ class FileWriterTests: XCTestCase { directory: directory, performance: PerformancePreset.mockAny(), dateProvider: SystemDateProvider(), - telemetry: NOPTelemetry() + telemetry: NOPTelemetry(), + metricsData: .init( + trackName: "rum", + consentLabel: .mockAny(), + uploaderPerformance: UploadPerformanceMock.noOp, + backgroundTasksEnabled: .mockAny() + ) ), encryption: nil, telemetry: NOPTelemetry() @@ -213,7 +231,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/Core/Upload/DataUploadStatusTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift index 91156d8a3d..6e826828df 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift @@ -11,85 +11,75 @@ import TestUtilities class DataUploadStatusTests: XCTestCase { // MARK: - Test `.needsRetry` - private let statusCodesExpectingNoRetry = [ - 202, // ACCEPTED - 400, // BAD REQUEST - 401, // UNAUTHORIZED - 403, // FORBIDDEN - 413, // PAYLOAD TOO LARGE + private let statusCodesExpectingNoRetry: [Int: String] = [ + 202: "accepted", + 400: "badRequest", + 401: "unauthorized", + 403: "forbidden", + 413: "payloadTooLarge", ] - private let statusCodesExpectingRetry = [ - 408, // REQUEST TIMEOUT - 429, // TOO MANY REQUESTS - 500, // INTERNAL SERVER ERROR - 502, // BAD GATEWAY - 503, // SERVICE UNAVAILABLE - 504, // GATEWAY TIMEOUT - 507, // INSUFFICIENT STORAGE + private let statusCodesExpectingRetry: [Int: String] = [ + 408: "requestTimeout", + 429: "tooManyRequests", + 500: "internalServerError", + 502: "badGateway", + 503: "serviceUnavailable", + 504: "gatewayTimeout", + 507: "insufficientStorage", ] private lazy var expectedStatusCodes = statusCodesExpectingNoRetry + statusCodesExpectingRetry func testWhenUploadFinishesWithResponse_andStatusCodeNeedsNoRetry_itSetsNeedsRetryFlagToFalse() { - statusCodesExpectingNoRetry.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + statusCodesExpectingNoRetry.forEach { statusCode, _ in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andStatusCodeNeedsRetry_itSetsNeedsRetryFlagToTrue() { - statusCodesExpectingRetry.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + statusCodesExpectingRetry.forEach { statusCode, _ in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertTrue(status.needsRetry, "Upload should be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andStatusCodeIsUnexpected_itSetsNeedsRetryFlagToFalse() { let allStatusCodes = Set((100...599)) - let unexpectedStatusCodes = allStatusCodes.subtracting(Set(expectedStatusCodes)) + let unexpectedStatusCodes = allStatusCodes.subtracting(Set(expectedStatusCodes.keys)) unexpectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny()) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockAny(), attempt: 0) XCTAssertFalse(status.needsRetry, "Upload should not be retried for status code \(statusCode)") } } func testWhenUploadFinishesWithError_itSetsNeedsRetryFlagToTrue() { - let status = DataUploadStatus(networkError: ErrorMock()) + let status = DataUploadStatus(networkError: ErrorMock(), attempt: 0) XCTAssertTrue(status.needsRetry, "Upload should be retried if it finished with error") } // MARK: - Test `.userDebugDescription` func testWhenUploadFinishesWithResponse_andRequestIDIsAvailable_itCreatesUserDebugDescription() { - expectedStatusCodes.forEach { statusCode in + expectedStatusCodes.forEach { statusCode, message in let requestID: String = .mockRandom(among: .alphanumerics) - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID) - XCTAssertTrue( - status.userDebugDescription.matches( - regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \(requestID)\\]" - ), - "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and request id '\(requestID)'" - ) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: requestID, attempt: 0) + XCTAssertEqual(status.userDebugDescription, "[response code: \(statusCode) (\(message)), request ID: \(requestID)") } } func testWhenUploadFinishesWithResponse_andRequestIDIsNotAvailable_itCreatesUserDebugDescription() { - expectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) - XCTAssertTrue( - status.userDebugDescription.matches( - regex: "\\[response code: [0-9]{3} \\([a-zA-Z]+\\), request ID: \\(\\?\\?\\?\\)\\]" - ), - "'\(status.userDebugDescription)' is not an expected description for status code '\(statusCode)' and no request id" - ) + expectedStatusCodes.forEach { statusCode, message in + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) + XCTAssertEqual(status.userDebugDescription, "[response code: \(statusCode) (\(message)), request ID: (???)") } } func testWhenUploadFinishesWithError_itCreatesUserDebugDescription() { let randomErrorDescription: String = .mockRandom() - let status = DataUploadStatus(networkError: ErrorMock(randomErrorDescription)) + let status = DataUploadStatus(networkError: ErrorMock(randomErrorDescription), attempt: 0) XCTAssertEqual(status.userDebugDescription, "[error: \(randomErrorDescription)]") } @@ -105,20 +95,20 @@ class DataUploadStatusTests: XCTestCase { ] func testWhenUploadFinishesWithResponse_andStatusCodeIs401_itCreatesError() { - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 401), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 401), ddRequestID: nil, attempt: 0) XCTAssertEqual(status.error, .unauthorized) } func testWhenUploadFinishesWithResponse_andStatusCodeIsDifferentThan401_itDoesNotCreateAnyError() { Set((100...599)).subtracting(alertingStatusCodes).forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error) } } func testWhenUploadFinishesWithResponse_andStatusCodeMeansSDKIssue_itCreatesHTTPError() { alertingStatusCodes.subtracting([401, 403]).forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockRandom()) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: .mockRandom(), attempt: 01) guard case let .httpError(statusCode: receivedStatusCode) = status.error else { return XCTFail("Upload status error should be created for status code: \(statusCode)") @@ -129,24 +119,24 @@ class DataUploadStatusTests: XCTestCase { } func testWhenUploadFinishesWithResponse_andStatusCodeMeansClientIssue_itDoesNotCreateHTTPError() { - let clientIssueStatusCodes = Set(expectedStatusCodes).subtracting(Set(alertingStatusCodes)) + let clientIssueStatusCodes = Set(expectedStatusCodes.keys).subtracting(Set(alertingStatusCodes)) clientIssueStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error, "Upload status error should not be created for status code \(statusCode)") } } func testWhenUploadFinishesWithResponse_andUnexpectedStatusCodeMeansClientIssue_itDoesNotCreateHTTPError() { - let unexpectedStatusCodes = Set((100...599)).subtracting(Set(expectedStatusCodes)) + let unexpectedStatusCodes = Set((100...599)).subtracting(Set(expectedStatusCodes.keys)) unexpectedStatusCodes.forEach { statusCode in - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: statusCode), ddRequestID: nil, attempt: 0) XCTAssertNil(status.error) } } func testWhenUploadFinishesWithError_andErrorCodeMeansSDKIssue_itCreatesNetworkError() throws { let alertingNSURLErrorCode = NSURLErrorBadURL - let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: alertingNSURLErrorCode, userInfo: nil)) + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: alertingNSURLErrorCode, userInfo: nil), attempt: 0) guard case let .networkError(error: nserror) = status.error else { return XCTFail("Upload status error should be created for NSURLError code: \(alertingNSURLErrorCode)") @@ -157,7 +147,7 @@ class DataUploadStatusTests: XCTestCase { func testWhenUploadFinishesWithError_andErrorCodeMeansExternalFactors_itDoesNotCreateNetworkError() { let notAlertingNSURLErrorCode = NSURLErrorNetworkConnectionLost - let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: notAlertingNSURLErrorCode, userInfo: nil)) + let status = DataUploadStatus(networkError: NSError(domain: NSURLErrorDomain, code: notAlertingNSURLErrorCode, userInfo: nil), attempt: 0) XCTAssertNil(status.error, "Upload status error should not be created for NSURLError code: \(notAlertingNSURLErrorCode)") } @@ -165,7 +155,7 @@ class DataUploadStatusTests: XCTestCase { func testWhenUploadFinishesWithResponse_itSetsResponseCode() { let randomCode: Int = .mockRandom(min: 1, max: 999) - let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: randomCode), ddRequestID: nil) + let status = DataUploadStatus(httpResponse: .mockResponseWith(statusCode: randomCode), ddRequestID: nil, attempt: 0) XCTAssertEqual(status.responseCode, randomCode) } } diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift index d87f82975d..f7ae3a6f8e 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -47,8 +47,15 @@ class DataUploadWorkerTests: XCTestCase { uploadExpectation.expectedFulfillmentCount = 3 let dataUploader = DataUploaderMock( - uploadStatus: DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 200), ddRequestID: nil), - onUpload: uploadExpectation.fulfill + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) // Given @@ -84,8 +91,15 @@ class DataUploadWorkerTests: XCTestCase { uploadExpectation.expectedFulfillmentCount = 2 let dataUploader = DataUploaderMock( - uploadStatus: DataUploadStatus(httpResponse: .mockResponseWith(statusCode: 200), ddRequestID: nil), - onUpload: uploadExpectation.fulfill + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) // Given @@ -120,7 +134,10 @@ class DataUploadWorkerTests: XCTestCase { let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: false)) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } // Given writer.write(value: ["key": "value"]) @@ -150,7 +167,8 @@ class DataUploadWorkerTests: XCTestCase { let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) - mockDataUploader.onUpload = { + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) initiatingUploadExpectation.fulfill() throw ErrorMock("Failed to prepare upload") } @@ -183,7 +201,10 @@ class DataUploadWorkerTests: XCTestCase { let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: true)) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } // Given writer.write(value: ["key": "value"]) @@ -209,6 +230,55 @@ class DataUploadWorkerTests: XCTestCase { XCTAssertEqual(try orchestrator.directory.files().count, 1, "When upload finishes with `needsRetry: true`, data should be preserved") } + func testGivenDataToUpload_whenUploadFinishesAndNeedsToBeRetried_thenPreviousUploadStatusIsNotNil() { + let startUploadExpectation = self.expectation(description: "Upload has started") + startUploadExpectation.expectedFulfillmentCount = 3 + + let mockDataUploader = DataUploaderMock( + uploadStatuses: [ + .mockWith(needsRetry: true, attempt: 0), + .mockWith(needsRetry: true, attempt: 1), + .mockWith(needsRetry: false, attempt: 2) + ] + ) + + var attempt: UInt = 0 + mockDataUploader.onUpload = { previousUploadStatus in + if attempt == 0 { + XCTAssertNil(previousUploadStatus) + } else { + XCTAssertNotNil(previousUploadStatus) + XCTAssertEqual(previousUploadStatus?.attempt, attempt - 1) + } + + attempt += 1 + startUploadExpectation.fulfill() + } + + // Given + writer.write(value: ["key": "value"]) + XCTAssertEqual(try orchestrator.directory.files().count, 1) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try orchestrator.directory.files().count, 0) + } + // MARK: - Upload Interval Changes func testWhenThereIsNoBatch_thenIntervalIncreases() { @@ -275,7 +345,8 @@ class DataUploadWorkerTests: XCTestCase { needsRetry: true, error: .httpError(statusCode: 500) ) - ) { + ) { previousUploadStatus in + XCTAssertNil(previousUploadStatus) uploadAttemptExpectation.fulfill() } @@ -352,7 +423,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -398,7 +472,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -433,7 +510,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -467,7 +547,10 @@ class DataUploadWorkerTests: XCTestCase { // When let startUploadExpectation = self.expectation(description: "Upload has started") let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) - mockDataUploader.onUpload = { startUploadExpectation.fulfill() } + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } let worker = DataUploadWorker( queue: uploaderQueue, @@ -500,7 +583,8 @@ class DataUploadWorkerTests: XCTestCase { // When let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) - mockDataUploader.onUpload = { + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) initiatingUploadExpectation.fulfill() throw ErrorMock("Failed to prepare upload") } @@ -565,7 +649,10 @@ class DataUploadWorkerTests: XCTestCase { let dataUploader = DataUploaderMock( uploadStatus: .mockWith(needsRetry: false), - onUpload: uploadExpectation.fulfill + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } ) let worker = DataUploadWorker( queue: uploaderQueue, diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift index 341f18aeab..d8884c70c7 100644 --- a/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift @@ -27,13 +27,15 @@ class DataUploaderTests: XCTestCase { // When let uploadStatus = try uploader.upload( events: .mockAny(), - context: .mockAny() + context: .mockAny(), + previous: nil ) // Then let expectedUploadStatus = DataUploadStatus( httpResponse: randomResponse, - ddRequestID: randomRequest.value(forHTTPHeaderField: "DD-REQUEST-ID") + ddRequestID: randomRequest.value(forHTTPHeaderField: "DD-REQUEST-ID"), + attempt: 0 ) DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) @@ -54,11 +56,12 @@ class DataUploaderTests: XCTestCase { // When let uploadStatus = try uploader.upload( events: .mockAny(), - context: .mockAny() + context: .mockAny(), + previous: nil ) // Then - let expectedUploadStatus = DataUploadStatus(networkError: randomError) + let expectedUploadStatus = DataUploadStatus(networkError: randomError, attempt: 0) DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) } @@ -73,7 +76,7 @@ class DataUploaderTests: XCTestCase { ) // When & Then - XCTAssertThrowsError(try uploader.upload(events: .mockAny(), context: .mockAny())) { error in + XCTAssertThrowsError(try uploader.upload(events: .mockAny(), context: .mockAny(), previous: nil)) { error in XCTAssertTrue(error is ErrorMock) } } diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift index f4199e6640..d79dfa4c3a 100644 --- a/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift @@ -266,7 +266,7 @@ class CrashReporterTests: XCTestCase { // MARK: - Thread safety - func testAllCallsToPluginAreSynchronized() { + func testInjectingContextToPluginAreSynchronized() { let expectation = self.expectation(description: "`plugin` received at least 100 calls") expectation.expectedFulfillmentCount = 100 expectation.assertForOverFulfill = false // to mitigate the call for initial context injection @@ -279,10 +279,6 @@ class CrashReporterTests: XCTestCase { mutableState.toggle() expectation.fulfill() } - plugin.didReadPendingCrashReport = { - mutableState.toggle() - expectation.fulfill() - } let crashContextProvider = CrashContextProviderMock(initialCrashContext: .mockRandom()) let feature = CrashReportingFeature( @@ -297,13 +293,14 @@ class CrashReporterTests: XCTestCase { callConcurrently( closures: [ { crashContextProvider.onCrashContextChange(.mockRandom()) }, - { feature.sendCrashReportIfFound() } + { crashContextProvider.onCrashContextChange(.mockRandom()) } ], iterations: 50 // each closure is called 50 times ) // swiftlint:enable opening_brace - waitForExpectations(timeout: 2, handler: nil) + feature.flush() + waitForExpectations(timeout: 0) } // MARK: - Usage diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift index 26c463970a..ffa82c98cf 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift @@ -26,9 +26,9 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), - dateProvider: SystemDateProvider(), - notificationCenter: notificationCenter + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: SystemDateProvider() ) // When @@ -57,9 +57,9 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), - dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0), - notificationCenter: notificationCenter + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0) ) var receivedHistoryStates: [AppState?] = [] diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift index fdd07afa3f..21465677f1 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift @@ -19,7 +19,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryState: .unknown) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.state, .charging) @@ -38,7 +38,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryLevel: 0.5) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.level, 0.75) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/DatadogContextProviderTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/DatadogContextProviderTests.swift index 2dedf378d9..c63b3de5b4 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/DatadogContextProviderTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/DatadogContextProviderTests.swift @@ -41,34 +41,6 @@ class DatadogContextProviderTests: XCTestCase { XCTAssertEqual(context.carrierInfo, carrierInfo) } - func testReaderPropagation() throws { - // Given - let serverOffsetReader = ContextValueReaderMock(initialValue: 0) - let networkConnectionInfoReader = ContextValueReaderMock() - let carrierInfoReader = ContextValueReaderMock() - - let provider = DatadogContextProvider(context: context) - provider.assign(reader: serverOffsetReader, to: \.serverTimeOffset) - provider.assign(reader: networkConnectionInfoReader, to: \.networkConnectionInfo) - provider.assign(reader: carrierInfoReader, to: \.carrierInfo) - - // When - let serverTimeOffset: TimeInterval = .mockRandomInThePast() - serverOffsetReader.value = serverTimeOffset - - let networkConnectionInfo: NetworkConnectionInfo = .mockRandom() - networkConnectionInfoReader.value = networkConnectionInfo - - let carrierInfo: CarrierInfo = .mockRandom() - carrierInfoReader.value = carrierInfo - - // Then - let context = provider.read() - XCTAssertEqual(context.serverTimeOffset, serverTimeOffset) - XCTAssertEqual(context.networkConnectionInfo, networkConnectionInfo) - XCTAssertEqual(context.carrierInfo, carrierInfo) - } - func testPublishNewContextOnValueChange() throws { let expectation = self.expectation(description: "publish new context") expectation.expectedFulfillmentCount = 3 @@ -91,25 +63,6 @@ class DatadogContextProviderTests: XCTestCase { wait(for: [expectation], timeout: 0.5) } - func testPublishContextOnContextRead() throws { - let expectation = self.expectation(description: "publish new context") - expectation.expectedFulfillmentCount = 3 - - // Given - - let provider = DatadogContextProvider(context: context) - provider.publish { _ in - expectation.fulfill() - } - - // When - (0..() let carrierInfoPublisher = ContextValuePublisherMock() - let serverOffsetReader = ContextValueReaderMock(initialValue: 0) - let networkConnectionInfoReader = ContextValueReaderMock() - let carrierInfoReader = ContextValueReaderMock() - let provider = DatadogContextProvider(context: context) provider.subscribe(\.serverTimeOffset, to: serverOffsetPublisher) provider.subscribe(\.networkConnectionInfo, to: networkConnectionInfoPublisher) provider.subscribe(\.carrierInfo, to: carrierInfoPublisher) - provider.assign(reader: serverOffsetReader, to: \.serverTimeOffset) - provider.assign(reader: networkConnectionInfoReader, to: \.networkConnectionInfo) - provider.assign(reader: carrierInfoReader, to: \.carrierInfo) - // swiftlint:disable opening_brace callConcurrently( closures: [ - { serverOffsetReader.value = .mockRandom() }, - { networkConnectionInfoReader.value = .mockRandom() }, - { carrierInfoReader.value = .mockRandom() }, { serverOffsetPublisher.value = .mockRandom() }, { networkConnectionInfoPublisher.value = .mockRandom() }, { carrierInfoPublisher.value = .mockRandom() }, diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift index 1eb2389e7e..7e79241b5c 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift @@ -17,8 +17,8 @@ class LowPowerModePublisherTests: XCTestCase { // Given let isLowPowerModeEnabled: Bool = .random() let publisher = LowPowerModePublisher( - processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled), - notificationCenter: notificationCenter + notificationCenter: notificationCenter, + processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled) ) XCTAssertEqual(publisher.initialValue, isLowPowerModeEnabled) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift index 9a51a52d7f..acf94fe544 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -78,6 +78,65 @@ class DatadogCoreTests: XCTestCase { XCTAssertEqual(requestBuilderSpy.requestParameters.count, 1, "It should send only one request") } + func testWhenWritingEventsWithPendingConsentThenGranted_itUploadsAllEvents() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .combining( + storagePerformance: StoragePerformanceMock.readAllFiles, + uploadPerformance: UploadPerformanceMock.veryQuick + ), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: 1, + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + let send2RequestsExpectation = expectation(description: "send 2 requests") + send2RequestsExpectation.expectedFulfillmentCount = 2 + + let requestBuilderSpy = FeatureRequestBuilderSpy() + requestBuilderSpy.onRequest = { _, _ in send2RequestsExpectation.fulfill() } + + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + + // When + let scope = core.scope(for: FeatureMock.self) + core.set(trackingConsent: .pending) + scope.eventWriteContext { context, writer in + XCTAssertEqual(context.trackingConsent, .pending) + writer.write(value: FeatureMock.Event(event: "pending")) + } + + core.set(trackingConsent: .granted) + scope.eventWriteContext { context, writer in + XCTAssertEqual(context.trackingConsent, .granted) + writer.write(value: FeatureMock.Event(event: "granted")) + } + + // Then + waitForExpectations(timeout: 2) + + let uploadedEvents = requestBuilderSpy.requestParameters + .flatMap { $0.events } + .map { $0.data.utf8String } + + XCTAssertEqual( + uploadedEvents, + [ + #"{"event":"pending"}"#, + #"{"event":"granted"}"# + ], + "It should upload all events" + ) + XCTAssertEqual(requestBuilderSpy.requestParameters.count, 2, "It should send 2 requests") + } + func testWhenWritingEventsWithBypassingConsent_itUploadsAllEvents() throws { // Given let core = DatadogCore( diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift index d1eb48fb65..221d3f0232 100644 --- a/DatadogCore/Tests/Datadog/DatadogTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -336,7 +336,16 @@ class DatadogTests: XCTestCase { try core.directory.getFeatureDirectories(forFeatureNamed: "tracing"), ] - let allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] } + let scope = core.scope(for: TraceFeature.self) + scope.dataStore.setValue("foo".data(using: .utf8)!, forKey: "bar") + + // Wait for async clear completion in all features: + core.readWriteQueue.sync {} + let tracingDataStoreDir = try core.directory.coreDirectory.subdirectory(path: core.directory.getDataStorePath(forFeatureNamed: "tracing")) + XCTAssertTrue(tracingDataStoreDir.hasFile(named: "bar")) + + var allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] } + allDirectories.append(.init(url: tracingDataStoreDir.url)) try allDirectories.forEach { directory in _ = try directory.createFile(named: .mockRandom()) } // When @@ -346,8 +355,11 @@ class DatadogTests: XCTestCase { core.readWriteQueue.sync {} // Then - let newNumberOfFiles = try allDirectories.reduce(0, { acc, nextDirectory in return try acc + nextDirectory.files().count }) - XCTAssertEqual(newNumberOfFiles, 0, "All files must be removed") + let files: [File] = allDirectories.reduce([], { acc, nextDirectory in + let next = try? nextDirectory.files() + return acc + (next ?? []) + }) + XCTAssertEqual(files, [], "All files must be removed") Datadog.flushAndDeinitialize() } diff --git a/DatadogCore/Tests/Datadog/LoggerTests.swift b/DatadogCore/Tests/Datadog/LoggerTests.swift index 2e0caaf7eb..96edc86dff 100644 --- a/DatadogCore/Tests/Datadog/LoggerTests.swift +++ b/DatadogCore/Tests/Datadog/LoggerTests.swift @@ -631,7 +631,7 @@ class LoggerTests: XCTestCase { try core.register(feature: logging) RUM.enable( - with: .mockWith { $0.sessionSampleRate = 100 }, + with: .mockWith { $0.sessionSampleRate = .maxSampleRate }, in: core ) @@ -986,7 +986,7 @@ class LoggerTests: XCTestCase { ) XCTAssertEqual( dd.logger.criticalLog?.error?.message, - "🔥 Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.builder.build()`." + "🔥 Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.create()`." ) XCTAssertTrue(logger is NOPLogger) } @@ -1009,7 +1009,7 @@ class LoggerTests: XCTestCase { ) XCTAssertEqual( dd.logger.criticalLog?.error?.message, - "🔥 Datadog SDK usage error: `Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + "🔥 Datadog SDK usage error: `Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." ) XCTAssertTrue(logger is NOPLogger) } diff --git a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift index 6691eab176..d374ccb3e9 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 { @@ -296,23 +297,36 @@ class NOPDataUploadWorker: DataUploadWorkerType { } internal class DataUploaderMock: DataUploaderType { - let uploadStatus: DataUploadStatus + let uploadStatuses: [DataUploadStatus] /// Notifies on each started upload. - var onUpload: (() throws -> Void)? + var onUpload: ((DataUploadStatus?) throws -> Void)? /// Tracks uploaded events. private(set) var uploadedEvents: [Event] = [] - init(uploadStatus: DataUploadStatus, onUpload: (() -> Void)? = nil) { - self.uploadStatus = uploadStatus + convenience init(uploadStatus: DataUploadStatus, onUpload: ((DataUploadStatus?) -> Void)? = nil) { + self.init(uploadStatuses: [uploadStatus], onUpload: onUpload) + } + + init(uploadStatuses: [DataUploadStatus], onUpload: ((DataUploadStatus?) -> Void)? = nil) { + self.uploadStatuses = uploadStatuses self.onUpload = onUpload } - func upload(events: [Event], context: DatadogContext) throws -> DataUploadStatus { - uploadedEvents += events - try onUpload?() - return uploadStatus + func upload( + events: [DatadogInternal.Event], + context: DatadogInternal.DatadogContext, + previous: DataUploadStatus?) throws -> DataUploadStatus { + uploadedEvents += events + try onUpload?(previous) + let attempt: UInt + if let previous = previous { + attempt = previous.attempt + 1 + } else { + attempt = 0 + } + return uploadStatuses[Int(attempt)] } } @@ -322,7 +336,8 @@ extension DataUploadStatus: RandomMockable { needsRetry: .random(), responseCode: .mockRandom(), userDebugDescription: .mockRandom(), - error: nil + error: nil, + attempt: .mockRandom() ) } @@ -330,13 +345,15 @@ extension DataUploadStatus: RandomMockable { needsRetry: Bool = .mockAny(), responseCode: Int = .mockAny(), userDebugDescription: String = .mockAny(), - error: DataUploadError? = nil + error: DataUploadError? = nil, + attempt: UInt = 0 ) -> DataUploadStatus { return DataUploadStatus( needsRetry: needsRetry, responseCode: responseCode, userDebugDescription: userDebugDescription, - error: error + error: error, + attempt: attempt ) } } diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 97566f0494..58a2bee8fa 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -49,6 +49,8 @@ internal class CrashReportingPluginMock: CrashReportingPlugin { var hasPurgedCrashReport: Bool? /// Custom app state data injected to the plugin. var injectedContextData: Data? + /// Custom backtrace reporter injected to the plugin. + var injectedBacktraceReporter: BacktraceReporting? func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) { hasPurgedCrashReport = completion(pendingCrashReport) @@ -65,11 +67,14 @@ internal class CrashReportingPluginMock: CrashReportingPlugin { /// Notifies the `inject(context:)` return. var didInjectContext: (() -> Void)? + + var backtraceReporter: BacktraceReporting? { injectedBacktraceReporter } } internal class NOPCrashReportingPlugin: CrashReportingPlugin { func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) {} func inject(context: Data) {} + var backtraceReporter: BacktraceReporting? { nil } } internal class CrashContextProviderMock: CrashContextProvider { @@ -93,6 +98,8 @@ class CrashReportSenderMock: CrashReportSender { } var didSendCrashReport: (() -> Void)? + + func send(launch: DatadogInternal.LaunchReport) {} } class RUMCrashReceiverMock: FeatureMessageReceiver { @@ -210,7 +217,8 @@ internal extension DDCrashReport { binaryImages: [BinaryImage] = [], meta: Meta = .mockAny(), wasTruncated: Bool = .mockAny(), - context: Data? = .mockAny() + context: Data? = .mockAny(), + additionalAttributes: [String: Encodable]? = nil ) -> DDCrashReport { return DDCrashReport( date: date, @@ -221,7 +229,8 @@ internal extension DDCrashReport { binaryImages: binaryImages, meta: meta, wasTruncated: wasTruncated, - context: context + context: context, + additionalAttributes: additionalAttributes ) } @@ -235,7 +244,8 @@ internal extension DDCrashReport { type: .mockRandom(), message: .mockRandom(), stack: .mockRandom(), - context: contextData + context: contextData, + additionalAttributes: mockRandomAttributes() ) } } diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValueReaderMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValueReaderMock.swift deleted file mode 100644 index 45685dbfff..0000000000 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValueReaderMock.swift +++ /dev/null @@ -1,43 +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 -@testable import DatadogCore - -internal class ContextValueReaderMock: ContextValueReader { - private let queue = DispatchQueue( - label: "com.datadoghq.context-value-reader-mock" - ) - - var value: Value { - get { queue.sync { _value } } - set { queue.sync { _value = newValue } } - } - - private var _value: Value - - init(initialValue: Value) { - self._value = initialValue - } - - init() where Value: ExpressibleByNilLiteral { - _value = nil - } - - func read(to receiver: inout Value) { - receiver = queue.sync { _value } - } -} - -extension ContextValueReader { - static func mockAny() -> ContextValueReaderMock where Value: ExpressibleByNilLiteral { - .init() - } - - static func mockWith(initialValue: Value) -> ContextValueReaderMock { - .init(initialValue: initialValue) - } -} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index 809b84f07c..d0ecde8792 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -36,20 +36,28 @@ internal class DatadogCoreProxy: DatadogCoreProtocol { @ReadWriteLock private var featureScopeInterceptors: [String: FeatureScopeInterceptor] = [:] - init(context: DatadogContext = .mockAny()) { - self.context = context - self.core = DatadogCore( - directory: temporaryCoreDirectory, - dateProvider: SystemDateProvider(), - initialConsent: context.trackingConsent, - performance: .mockAny(), - httpClient: HTTPClientMock(), - encryption: nil, - contextProvider: DatadogContextProvider(context: context), - applicationVersion: context.version, - maxBatchesPerUpload: .mockRandom(min: 1, max: 100), - backgroundTasksEnabled: .mockAny() + convenience init(context: DatadogContext = .mockAny()) { + self.init( + core: DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: context.trackingConsent, + performance: .mockAny(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: DatadogContextProvider( + context: context + ), + applicationVersion: context.version, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) ) + } + + init(core: DatadogCore) { + self.context = core.contextProvider.read() + self.core = core // override the message-bus's core instance core.bus.connect(core: self) @@ -91,6 +99,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 { @@ -142,7 +154,7 @@ private struct FeatureScopeProxy: FeatureScope { } } -private class FeatureScopeInterceptor { +private final class FeatureScopeInterceptor: @unchecked Sendable { struct InterceptingWriter: Writer { static let jsonEncoder = JSONEncoder.dd.default() diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift index 5009e59f55..7b8f9e4f97 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift @@ -19,17 +19,31 @@ internal class FeatureRequestBuilderMock: FeatureRequestBuilder { self.init(factory: { _, _ in request }) } - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { return try factory(events, context) } } internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { - /// Records parameters passed to `requet(for:with:)` + /// Stores the parameters passed to the `request(for:with:)` method. + @ReadWriteLock private(set) var requestParameters: [(events: [Event], context: DatadogContext)] = [] - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + /// A closure that is called when a request is about to be created in the `request(for:with:)` method. + @ReadWriteLock + var onRequest: ((_ events: [Event], _ context: DatadogContext) -> Void)? + + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { requestParameters.append((events: events, context: context)) + onRequest?(events, context) return .mockAny() } } @@ -37,7 +51,11 @@ internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { internal struct FailingRequestBuilderMock: FeatureRequestBuilder { let error: Error - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { throw error } } diff --git a/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift b/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift index 430367f0a4..e03165cf2c 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift @@ -70,9 +70,9 @@ extension Directory { do { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes) let initialFilesCount = try files().count - XCTAssert(initialFilesCount == 0, "🔥 `TestsDirectory` is not empty: \(url)", file: file, line: line) + XCTAssert(initialFilesCount == 0, "🔥 `Directory` is not empty: \(url)", file: file, line: line) } catch { - XCTFail("🔥 Failed to create `TestsDirectory`: \(error)", file: file, line: line) + XCTFail("🔥 `Directory.create()` failed: \(error)", file: file, line: line) } return self } @@ -83,7 +83,7 @@ extension Directory { do { try FileManager.default.removeItem(at: url) } catch { - XCTFail("🔥 Failed to delete `TestsDirectory`: \(error)", file: file, line: line) + XCTFail("🔥 `Directory.delete()` failed: \(error)", file: file, line: line) } } } diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index 3505464f39..2936adc277 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -11,6 +11,7 @@ import TestUtilities extension RUMUser { static func mockRandom() -> RUMUser { return RUMUser( + anonymousId: .mockRandom(), email: .mockRandom(), id: .mockRandom(), name: .mockRandom(), @@ -190,6 +191,7 @@ extension RUMViewEvent: RandomMockable { interactionToNextPaint: nil, interactionToNextPaintTargetSelector: nil, interactionToNextPaintTime: .mockRandom(), + interactionToNextViewTime: nil, isActive: viewIsActive, isSlowRendered: .mockRandom(), jsRefreshRate: nil, @@ -202,6 +204,7 @@ extension RUMViewEvent: RandomMockable { memoryAverage: .mockRandom(), memoryMax: .mockRandom(), name: .mockRandom(), + networkSettledTime: nil, referrer: .mockRandom(), refreshRateAverage: .mockRandom(), refreshRateMin: .mockRandom(), @@ -249,6 +252,7 @@ extension RUMResourceEvent: RandomMockable { resource: .init( connect: .init(duration: .mockRandom(), start: .mockRandom()), decodedBodySize: nil, + deliveryType: nil, dns: .init(duration: .mockRandom(), start: .mockRandom()), download: .init(duration: .mockRandom(), start: .mockRandom()), duration: .mockRandom(), @@ -256,6 +260,7 @@ extension RUMResourceEvent: RandomMockable { firstByte: .init(duration: .mockRandom(), start: .mockRandom()), id: .mockRandom(), method: .mockRandom(), + protocol: nil, provider: .init( domain: .mockRandom(), name: .mockRandom(), @@ -268,7 +273,8 @@ extension RUMResourceEvent: RandomMockable { statusCode: .mockRandom(), transferSize: nil, type: [.native, .image].randomElement()!, - url: .mockRandom() + url: .mockRandom(), + worker: nil ), service: .mockRandom(), session: .init( @@ -469,7 +475,18 @@ extension RUMLongTaskEvent: RandomMockable { date: .mockRandom(), device: .mockRandom(), display: nil, - longTask: .init(duration: .mockRandom(), id: .mockRandom(), isFrozenFrame: .mockRandom()), + longTask: .init( + blockingDuration: nil, + duration: .mockRandom(), + entryType: nil, + firstUiEventTimestamp: nil, + id: .mockRandom(), + isFrozenFrame: .mockRandom(), + renderStart: nil, + scripts: nil, + startTime: nil, + styleAndLayoutStart: nil + ), os: .mockRandom(), service: .mockRandom(), session: .init( @@ -493,6 +510,7 @@ extension TelemetryConfigurationEvent: RandomMockable { action: .init(id: .mockRandom()), application: .init(id: .mockRandom()), date: .mockRandom(), + effectiveSampleRate: .mockRandom(), experimentalFeatures: nil, service: .mockRandom(), session: .init(id: .mockRandom()), @@ -507,12 +525,14 @@ extension TelemetryConfigurationEvent: RandomMockable { batchProcessingLevel: .mockRandom(), batchSize: .mockAny(), batchUploadFrequency: .mockAny(), + collectFeatureFlagsOn: nil, compressIntakeRequests: nil, defaultPrivacyLevel: .mockAny(), forwardConsoleLogs: nil, forwardErrorsToLogs: nil, forwardReports: nil, initializationType: nil, + isMainProcess: nil, mobileVitalsUpdatePeriod: .mockRandom(), premiumSampleRate: nil, reactNativeVersion: nil, diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index 9642415537..7ef7b71127 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -41,7 +41,7 @@ extension WebViewEventReceiver: AnyMockable { featureScope: FeatureScope = NOPFeatureScope(), dateProvider: DateProvider = SystemDateProvider(), commandSubscriber: RUMCommandSubscriber = RUMCommandSubscriberMock(), - viewCache: ViewCache = ViewCache() + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()) ) -> Self { .init( featureScope: featureScope, @@ -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 ) } } @@ -504,11 +507,12 @@ extension RUMStartUserActionCommand: AnyMockable, RandomMockable { static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], + instrumentation: InstrumentationType = .manual, actionType: RUMActionType = .swipe, name: String = .mockAny() ) -> RUMStartUserActionCommand { return RUMStartUserActionCommand( - time: time, attributes: attributes, actionType: actionType, name: name + time: time, attributes: attributes, instrumentation: instrumentation, actionType: actionType, name: name ) } } @@ -552,11 +556,12 @@ extension RUMAddUserActionCommand: AnyMockable, RandomMockable { static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], + instrumentation: InstrumentationType = .manual, actionType: RUMActionType = .tap, name: String = .mockAny() ) -> RUMAddUserActionCommand { return RUMAddUserActionCommand( - time: time, attributes: attributes, actionType: actionType, name: name + time: time, attributes: attributes, instrumentation: instrumentation, actionType: actionType, name: name ) } } @@ -724,9 +729,10 @@ extension RUMScopeDependencies { syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), - sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry()) + sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), + watchdogTermination: WatchdogTerminationMonitor? = nil ) -> RUMScopeDependencies { return RUMScopeDependencies( featureScope: featureScope, @@ -744,7 +750,8 @@ extension RUMScopeDependencies { onSessionStart: onSessionStart, viewCache: viewCache, fatalErrorContext: fatalErrorContext, - sessionEndedMetric: sessionEndedMetric + sessionEndedMetric: sessionEndedMetric, + watchdogTermination: watchdogTermination ) } @@ -764,7 +771,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 +790,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 ) } } @@ -798,7 +807,7 @@ extension RUMSessionScope { return mockWith() } - // swiftlint:disable:next function_default_parameter_at_end + // swiftlint:disable function_default_parameter_at_end static func mockWith( isInitialSession: Bool = .mockAny(), parent: RUMContextProvider = RUMContextProviderMock(), @@ -817,6 +826,7 @@ extension RUMSessionScope { dependencies: dependencies ) } + // swiftlint:enable function_default_parameter_at_end } private let mockWindow = UIWindow(frame: .zero) @@ -929,6 +939,7 @@ extension RUMUserActionScope { startTime: Date = .mockAny(), serverTimeOffset: TimeInterval = .zero, isContinuous: Bool = .mockAny(), + instrumentation: InstrumentationType = .manual, onActionEventSent: @escaping (RUMActionEvent) -> Void = { _ in } ) -> RUMUserActionScope { return RUMUserActionScope( @@ -940,6 +951,7 @@ extension RUMUserActionScope { startTime: startTime, serverTimeOffset: serverTimeOffset, isContinuous: isContinuous, + instrumentation: instrumentation, onActionEventSent: onActionEventSent ) } @@ -1030,9 +1042,10 @@ class UIPressRUMActionsPredicateMock: UIPressRUMActionsPredicate { } } -class UIKitRUMUserActionsHandlerMock: UIEventHandler { +class RUMActionsHandlerMock: RUMActionsHandling { var onSubscribe: ((RUMCommandSubscriber) -> Void)? var onSendEvent: ((UIApplication, UIEvent) -> Void)? + var onViewModifierTapped: ((String, [String: any Encodable]) -> Void)? func publish(to subscriber: RUMCommandSubscriber) { onSubscribe?(subscriber) @@ -1041,6 +1054,10 @@ class UIKitRUMUserActionsHandlerMock: UIEventHandler { func notify_sendEvent(application: UIApplication, event: UIEvent) { onSendEvent?(application, event) } + + func notify_viewModifierTapped(actionName: String, actionAttributes: [String: any Encodable]) { + onViewModifierTapped?(actionName, actionAttributes) + } } class SamplingBasedVitalReaderMock: SamplingBasedVitalReader { diff --git a/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift index f1d36ec394..df66086bea 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMFeatureTests.swift @@ -91,7 +91,7 @@ class RUMFeatureTests: XCTestCase { XCTAssertEqual( requestURL.query, """ - ddsource=\(randomSource)&ddtags=service:\(randomServiceName),version:\(randomApplicationVersion),sdk_version:\(randomSDKVersion),env:\(randomEnvironmentName) + ddsource=\(randomSource)&ddtags=service:\(randomServiceName),version:\(randomApplicationVersion),sdk_version:\(randomSDKVersion),env:\(randomEnvironmentName),retry_count:1 """ ) XCTAssertEqual( diff --git a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift index 98a79ed1c7..c43eac2801 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMMonitorTests.swift @@ -35,7 +35,7 @@ class RUMMonitorTests: XCTestCase { // Given var capturedSession: String? config.dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) - config.sessionSampleRate = 100.0 + config.sessionSampleRate = .maxSampleRate config.onSessionStart = { session, sampled in capturedSession = session } @@ -75,7 +75,7 @@ class RUMMonitorTests: XCTestCase { func testWhenSessionIsStopped_itReturnsNil() throws { // Given config.dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) - config.sessionSampleRate = 100.0 + config.sessionSampleRate = .maxSampleRate RUM.enable(with: config, in: core) let monitor = RUMMonitor.shared(in: core) @@ -642,7 +642,7 @@ class RUMMonitorTests: XCTestCase { monitor.stopView(viewController: mockView) let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() - let expectedUserInfo = RUMUser(email: "foo@bar.com", id: "abc-123", name: "Foo", usrInfo: [ + let expectedUserInfo = RUMUser(anonymousId: nil, email: "foo@bar.com", id: "abc-123", name: "Foo", usrInfo: [ "str": AnyEncodable("value"), "int": AnyEncodable(11_235), "bool": AnyEncodable(true) diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalCPUReaderTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalCPUReaderTests.swift index 564c769a65..a2dae97c8a 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalCPUReaderTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalCPUReaderTests.swift @@ -13,15 +13,28 @@ class VitalCPUReaderTest: XCTestCase { lazy var cpuReader = VitalCPUReader(notificationCenter: testNotificationCenter) func testWhenCPUUnderHeavyLoadItMeasuresHigherCPUTicks() throws { - let highLoadAverage = try averageCPUTicks { - heavyLoad() + let repetitions = 3 + + // The CPU ticks consumed during heavy processing are not always greater than those during light processing. + // System variables can influence these results, leading to potential inaccuracies. + // To minimize false positives, an average is calculated over n repetitions. + let utilizationArray: [(highUtilization: Double, sleepUtilization: Double)] = try (0.. Void) throws -> Double { - let startDate = Date() + private func utilizationAndDuration(_ block: () -> Void) throws -> (utilization: Double, duration: Double) { + let startTime = CFAbsoluteTimeGetCurrent() let startUtilization = try XCTUnwrap(cpuReader.readVitalData()) block() let endUtilization = try XCTUnwrap(cpuReader.readVitalData()) - let duration = Date().timeIntervalSince(startDate) + let duration = CFAbsoluteTimeGetCurrent() - startTime let utilizedTicks = endUtilization - startUtilization - let utilization = utilizedTicks / duration - return utilization + return (utilizedTicks / duration, duration) } } diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift index 26df699b63..aaafdfc184 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift @@ -99,9 +99,9 @@ class VitalInfoSamplerTests: XCTestCase { DispatchQueue.global().sync { // in real-world scenarios, sampling will be started from background threads sampler = VitalInfoSampler( - cpuReader: VitalCPUReader(), + cpuReader: VitalCPUReader(notificationCenter: .default), memoryReader: VitalMemoryReader(), - refreshRateReader: VitalRefreshRateReader(), + refreshRateReader: VitalRefreshRateReader(notificationCenter: .default), frequency: 0.1 ) } diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift index 09707f7c1a..357c5bc23e 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift @@ -220,6 +220,34 @@ class VitalRefreshRateReaderTests: XCTestCase { let thirdFps = reader.framesPerSecond(provider: frameInfoProvider) XCTAssertEqual(thirdFps, 42.85714285714286) } + + /* Rate representation + * + * 0----------8ms---------16ms--------24ms--------32ms + * | 6ms | 6ms | 6ms | 6ms | + * + */ + func testFramesPerSecond_givenAdaptiveSyncDisplayWithQuickerThanExpectedFrames() { + let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) + var frameInfoProvider = FrameInfoProviderMock(maximumDeviceFramesPerSecond: 120) + + // first frame recorded + frameInfoProvider.currentFrameTimestamp = 0 + frameInfoProvider.nextFrameTimestamp = 0.008 + let firstFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertNil(firstFps) + + // second frame recorded + frameInfoProvider.currentFrameTimestamp = 0.006 + frameInfoProvider.nextFrameTimestamp = 0.014 + let secondFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(secondFps, 60) + + // third frame recorded + frameInfoProvider.currentFrameTimestamp = 0.012 + let thirdFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(thirdFps, 60) + } } struct FrameInfoProviderMock: FrameInfoProvider { diff --git a/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift b/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift index d111411151..3f411d50d0 100644 --- a/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift @@ -11,7 +11,7 @@ import XCTest #if !os(tvOS) class UIApplicationSwizzlerTests: XCTestCase { - private let handler = UIKitRUMUserActionsHandlerMock() + private let handler = RUMActionsHandlerMock() private lazy var swizzler = try! UIApplicationSwizzler(handler: handler) override func setUp() { diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index ae7d7fb263..94ac387268 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -41,7 +41,14 @@ class TracerTests: XCTestCase { source: "abc", sdkVersion: "1.2.3", ciAppOrigin: nil, - applicationBundleIdentifier: "com.datadoghq.ios-sdk" + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + device: .mockWith( + name: "iPhone", + model: "iPhone10,1", + osVersion: "15.4.1", + osBuildNumber: "13D20", + architecture: "arm64" + ) ) config.dateProvider = RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) config.traceIDGenerator = RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)) @@ -71,6 +78,15 @@ class TracerTests: XCTestCase { "type": "custom", "meta.tracer.version": "1.2.3", "meta.version": "1.0.0", + "meta.device.architecture": "arm64", + "meta.device.brand": "Apple", + "meta.device.model": "iPhone10,1", + "meta.device.name": "iPhone", + "meta.device.type": "mobile", + "meta.os.build": "13D20", + "meta.os.name": "iOS", + "meta.os.version": "15.4.1", + "meta.os.version_major": "15", "meta._dd.source": "abc", "metrics._top_level": 1, "metrics._sampling_priority_v1": 1, diff --git a/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift index 2c163c9c5f..ca87c74455 100644 --- a/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift @@ -84,7 +84,8 @@ class DatadogTraceFeatureTests: XCTestCase { let request = server.waitAndReturnRequests(count: 1)[0] let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(request.httpMethod, "POST") - XCTAssertEqual(requestURL.absoluteString, randomUploadURL.absoluteString) + XCTAssertEqual(requestURL.host, randomUploadURL.host) + XCTAssertEqual(requestURL.path, randomUploadURL.path) XCTAssertNil(requestURL.query) XCTAssertEqual( request.allHTTPHeaderFields?["User-Agent"], diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index 8b88bf6b4f..66c2860978 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -109,12 +109,13 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.tags[OTTags.httpUrl], request.url!.absoluteString) XCTAssertEqual(span.tags[OTTags.httpMethod], "GET") XCTAssertEqual(span.tags[SpanTags.errorType], "domain - 123") + XCTAssertEqual(span.tags[SpanTags.kind], "client") XCTAssertEqual( span.tags[SpanTags.errorStack], "Error Domain=domain Code=123 \"network error\" UserInfo={NSLocalizedDescription=network error}" ) XCTAssertEqual(span.tags[SpanTags.errorMessage], "network error") - XCTAssertEqual(span.tags.count, 7) + XCTAssertEqual(span.tags.count, 8) let log: LogEvent = try XCTUnwrap(core.events().last, "It should send error log") XCTAssertEqual(log.status, .error) @@ -178,11 +179,12 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.tags[OTTags.httpStatusCode], "404") XCTAssertEqual(span.tags[SpanTags.errorType], "HTTPURLResponse - 404") XCTAssertEqual(span.tags[SpanTags.errorMessage], "404 not found") + XCTAssertEqual(span.tags[SpanTags.kind], "client") XCTAssertEqual( span.tags[SpanTags.errorStack], "Error Domain=HTTPURLResponse Code=404 \"404 not found\" UserInfo={NSLocalizedDescription=404 not found}" ) - XCTAssertEqual(span.tags.count, 8) + XCTAssertEqual(span.tags.count, 9) let log: LogEvent = try XCTUnwrap(core.events().last, "It should send error log") XCTAssertEqual(log.status, .error) diff --git a/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift index 5f274371ca..18ee3cb215 100644 --- a/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift @@ -24,6 +24,7 @@ class DDConfigurationTests: XCTestCase { XCTAssertEqual(objcConfig.sdkConfiguration.additionalConfiguration.count, 0) XCTAssertNil(objcConfig.sdkConfiguration.encryption) XCTAssertNotNil(objcConfig.sdkConfiguration.serverDateProvider) + XCTAssertFalse(objcConfig.sdkConfiguration.backgroundTasksEnabled) } func testCustomizedBuilderForwardsInitializationToSwift() throws { @@ -92,6 +93,10 @@ class DDConfigurationTests: XCTestCase { let serverDateProvider = ObjcServerDateProvider() objcConfig.setServerDateProvider(serverDateProvider) XCTAssertTrue((objcConfig.sdkConfiguration.serverDateProvider as? DDServerDateProviderBridge)?.objcProvider === serverDateProvider) + + let fakeBackgroundTasksEnabled: Bool = .mockRandom() + objcConfig.backgroundTasksEnabled = fakeBackgroundTasksEnabled + XCTAssertEqual(objcConfig.sdkConfiguration.backgroundTasksEnabled, fakeBackgroundTasksEnabled) } func testDataEncryption() throws { diff --git a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift index 40473a017a..1f7e7f7d87 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"]?.dd.decode(), 42) + XCTAssertEqual(extraInfo["attribute-double"]?.dd.decode(), 42.5) + XCTAssertEqual(extraInfo["attribute-string"]?.dd.decode(), "string value") + XCTAssertEqual(extraInfo["foo"]?.dd.decode(), "bar") DDDatadog.setUserInfo(id: nil, name: nil, email: nil, extraInfo: [:]) XCTAssertNil(userInfo.current.id) diff --git a/DatadogCore/Tests/DatadogObjc/DDInternalLoggerTests.swift b/DatadogCore/Tests/DatadogObjc/DDInternalLoggerTests.swift new file mode 100644 index 0000000000..ffb2074a5d --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDInternalLoggerTests.swift @@ -0,0 +1,89 @@ +/* +* 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 +@testable import DatadogCore +@testable import DatadogObjc + +class DDInternalLoggerTests: XCTestCase { + let telemetry = TelemetryReceiverMock() + + private var core: PassthroughCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = PassthroughCoreMock(messageReceiver: telemetry) + } + + override func tearDown() { + core = nil + super.tearDown() + } + + func testObjcTelemetryDebugCallsTelemetryDebug() throws { + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // Given + let id: String = .mockAny() + let message: String = .mockAny() + + // When + DDInternalLogger.telemetryDebug(id: id, message: message) + + // Then + XCTAssertEqual(telemetry.messages.count, 1) + let debug = try XCTUnwrap(telemetry.messages.first?.asDebug, "A debug should be send to `telemetry`.") + XCTAssertEqual(debug.id, id) + XCTAssertEqual(debug.message, message) + } + + func testObjcTelemetryErrorCallsTelemetryError() throws { + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // Given + let id: String = .mockAny() + let message: String = .mockAny() + let stack: String = .mockAny() + let kind: String = .mockAny() + + // When + DDInternalLogger.telemetryError(id: id, message: message, kind: kind, stack: stack) + + // Then + XCTAssertEqual(telemetry.messages.count, 1) + + let error = try XCTUnwrap(telemetry.messages.first?.asError, "An error should be send to `telemetry`.") + XCTAssertEqual(error.id, id) + XCTAssertEqual(error.message, message) + XCTAssertEqual(error.kind, kind) + XCTAssertEqual(error.stack, stack) + } + + func testWhenTelemetryIsSentThroughObjc_thenItForwardsToDDTelemetry() throws { + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // When + let randomDebugMessage: String = .mockRandom() + let randomErrorMessage: String = .mockRandom() + DDInternalLogger.telemetryDebug(id: .mockAny(), message: randomDebugMessage) + DDInternalLogger.telemetryError(id: .mockAny(), message: randomErrorMessage, kind: .mockAny(), stack: .mockAny()) + + // Then + XCTAssertEqual(telemetry.messages.count, 2) + + let debug = try XCTUnwrap(telemetry.messages.first?.asDebug, "A debug should be send to `telemetry`.") + XCTAssertEqual(debug.message, randomDebugMessage) + + let error = try XCTUnwrap(telemetry.messages.last?.asError, "An error should be send to `telemetry`.") + XCTAssertEqual(error.message, randomErrorMessage) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift b/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift index 8eb14f2f6e..f4a5eac7a7 100644 --- a/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift @@ -271,6 +271,24 @@ class DDLogsTests: XCTestCase { XCTAssertEqual(objcConfig.configuration.remoteSampleRate, 50) XCTAssertNotNil(objcConfig.configuration.consoleLogFormat) } + + func testEventMapping() throws { + let logsConfiguration = DDLogsConfiguration() + logsConfiguration.setEventMapper { logEvent in + logEvent.message = "custom-log-message" + logEvent.attributes.userAttributes["custom-attribute"] = "custom-value" + return logEvent + } + DDLogs.enable(with: logsConfiguration) + + let objcLogger = DDLogger.create() + + objcLogger.debug("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertMessage(equals: "custom-log-message") + logMatchers[0].assertAttributes(equal: ["custom-attribute": "custom-value"]) + } } // swiftlint:enable multiline_arguments_brackets // swiftlint:enable compiler_protocol_init 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..059b376c1a 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"]?.dd.decode(), "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"]?.dd.decode(), "bar") XCTAssertEqual(objcRUMAction.name, "name") XCTAssertEqual(objcRUMAction.attributes["foo"] as? String, "bar") } diff --git a/DatadogCore/Tests/DatadogObjc/DDSessionReplayTests.swift b/DatadogCore/Tests/DatadogObjc/DDSessionReplayTests.swift deleted file mode 100644 index 9480907419..0000000000 --- a/DatadogCore/Tests/DatadogObjc/DDSessionReplayTests.swift +++ /dev/null @@ -1,77 +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. - */ - -#if os(iOS) - -import XCTest -import TestUtilities -import DatadogInternal - -@testable import DatadogSessionReplay - -class DDSessionReplayTests: XCTestCase { - func testDefaultConfiguration() { - // Given - let sampleRate: Float = .mockRandom(min: 0, max: 100) - - // When - let config = DDSessionReplayConfiguration(replaySampleRate: sampleRate) - - // Then - XCTAssertEqual(config._swift.replaySampleRate, sampleRate) - XCTAssertEqual(config._swift.defaultPrivacyLevel, .mask) - XCTAssertNil(config._swift.customEndpoint) - } - - func testConfigurationOverrides() { - // Given - let sampleRate: Float = .mockRandom(min: 0, max: 100) - let privacy: DDSessionReplayConfigurationPrivacyLevel = [.allow, .mask, .maskUserInput].randomElement()! - let url: URL = .mockRandom() - - // When - let config = DDSessionReplayConfiguration(replaySampleRate: 100) - config.replaySampleRate = sampleRate - config.defaultPrivacyLevel = privacy - config.customEndpoint = url - - // Then - XCTAssertEqual(config._swift.replaySampleRate, sampleRate) - XCTAssertEqual(config._swift.defaultPrivacyLevel, privacy._swift) - XCTAssertEqual(config._swift.customEndpoint, url) - } - - func testPrivacyLevelsInterop() { - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel.allow._swift, .allow) - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel.mask._swift, .mask) - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel.maskUserInput._swift, .maskUserInput) - - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel(.allow), .allow) - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel(.mask), .mask) - XCTAssertEqual(DDSessionReplayConfigurationPrivacyLevel(.maskUserInput), .maskUserInput) - } - - func testWhenEnabled() throws { - // Given - let core = FeatureRegistrationCoreMock() - CoreRegistry.register(default: core) - defer { CoreRegistry.unregisterDefault() } - - let config = DDSessionReplayConfiguration(replaySampleRate: 42) - - // When - DDSessionReplay.enable(with: config) - - // Then - let sr = try XCTUnwrap(core.get(feature: SessionReplayFeature.self)) - let requestBuilder = try XCTUnwrap(sr.requestBuilder as? DatadogSessionReplay.SegmentRequestBuilder) - XCTAssertEqual(sr.recordingCoordinator.sampler.samplingRate, 42) - XCTAssertEqual(sr.recordingCoordinator.privacy, .mask) - XCTAssertNil(requestBuilder.customUploadURL) - } -} - -#endif diff --git a/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift index 16957ca9a5..7e4aff6885 100644 --- a/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift @@ -6,6 +6,7 @@ import XCTest import TestUtilities +import DatadogInternal @testable import DatadogTrace @testable import DatadogObjc @@ -26,10 +27,10 @@ class DDTraceConfigurationTests: XCTestCase { } func testTags() { - let random = mockRandomAttributes() + let random: [String: Any] = mockRandomAttributes() objc.tags = random - DDAssertDictionariesEqual(objc.tags!, random) - DDAssertReflectionEqual(swift.tags!, castAttributesToSwift(random)) + DDAssertJSONEqual(objc.tags!, random) + DDAssertReflectionEqual(swift.tags!, random.dd.swiftAttributes) } func testSetDDTraceURLSessionTracking() { diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m index 1b5bedcb3d..f4f766163c 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m @@ -65,6 +65,7 @@ - (void)testDDConfigurationBuilderAPI { configuration.uploadFrequency = DDUploadFrequencyAverage; configuration.additionalConfiguration = @{@"additional": @"config"}; [configuration setEncryption:[CustomDDDataEncryption new]]; + configuration.backgroundTasksEnabled = true; } - (void)testDatadogCrashReporterAPI { diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDInternalLogger+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDInternalLogger+apiTests.m new file mode 100644 index 0000000000..dd19b37dcd --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDInternalLogger+apiTests.m @@ -0,0 +1,25 @@ +/* +* 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 +@import DatadogObjc; + +@interface DDInternalLogger_apiTests : XCTestCase +@end + +/* + * `DDInternalLogger` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDInternalLogger_apiTests + +- (void)testDDInternalLogger { + + [DDInternalLogger consolePrint:@"" :DDCoreLoggerLevelWarn]; + [DDInternalLogger telemetryDebugWithId:@"" message:@""]; + [DDInternalLogger telemetryErrorWithId:@"" message:@"" kind:@"" stack:@""]; +} + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m index 4b69f5fa14..bdbe99fba4 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m @@ -13,12 +13,84 @@ @interface DDSessionReplay_apiTests : XCTestCase @implementation DDSessionReplay_apiTests -- (void)testConfiguration { +// MARK: Configuration +- (void)testConfigurationDeprecatedApi __attribute__ ((deprecated)) { DDSessionReplayConfiguration *configuration = [[DDSessionReplayConfiguration alloc] initWithReplaySampleRate:100]; configuration.defaultPrivacyLevel = DDSessionReplayConfigurationPrivacyLevelAllow; + + [DDSessionReplay enableWith:configuration]; +} + +- (void)testConfigurationWithNewApi { + DDSessionReplayConfiguration *configuration = [[DDSessionReplayConfiguration alloc] initWithReplaySampleRate:100 + textAndInputPrivacyLevel:DDTextAndInputPrivacyLevelMaskAll + imagePrivacyLevel:DDImagePrivacyLevelMaskNone + touchPrivacyLevel:DDTouchPrivacyLevelShow + featureFlags:nil]; configuration.customEndpoint = [NSURL new]; + configuration.textAndInputPrivacyLevel = DDTextAndInputPrivacyLevelMaskSensitiveInputs; + configuration.imagePrivacyLevel = DDImagePrivacyLevelMaskAll; + configuration.touchPrivacyLevel = DDTouchPrivacyLevelHide; + [DDSessionReplay enableWith:configuration]; } +- (void)testStartAndStopRecording { + [DDSessionReplay startRecording]; + [DDSessionReplay stopRecording]; +} + +- (void)testStartRecordingImmediately { + DDSessionReplayConfiguration *configuration = [[DDSessionReplayConfiguration alloc] initWithReplaySampleRate:100 + textAndInputPrivacyLevel:DDTextAndInputPrivacyLevelMaskAll + imagePrivacyLevel:DDImagePrivacyLevelMaskAll + touchPrivacyLevel:DDTouchPrivacyLevelHide + featureFlags:nil]; + + configuration.startRecordingImmediately = false; + + XCTAssertFalse(configuration.startRecordingImmediately); +} + +// MARK: Privacy Overrides +- (void)testSettingAndGettingOverrides { + // Given + UIView *view = [[UIView alloc] init]; + + // When + view.ddSessionReplayPrivacyOverrides.textAndInputPrivacy = DDTextAndInputPrivacyLevelOverrideMaskAll; + view.ddSessionReplayPrivacyOverrides.imagePrivacy = DDImagePrivacyLevelOverrideMaskAll; + view.ddSessionReplayPrivacyOverrides.touchPrivacy = DDTouchPrivacyLevelOverrideHide; + view.ddSessionReplayPrivacyOverrides.hide = @YES; + + // Then + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.textAndInputPrivacy, DDTextAndInputPrivacyLevelOverrideMaskAll); + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.imagePrivacy, DDImagePrivacyLevelOverrideMaskAll); + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.touchPrivacy, DDTouchPrivacyLevelOverrideHide); + XCTAssertTrue(view.ddSessionReplayPrivacyOverrides.hide.boolValue); +} + +- (void)testClearingOverride { + // Given + UIView *view = [[UIView alloc] init]; + + // Set initial values + view.ddSessionReplayPrivacyOverrides.textAndInputPrivacy = DDTextAndInputPrivacyLevelOverrideMaskAll; + view.ddSessionReplayPrivacyOverrides.imagePrivacy = DDImagePrivacyLevelOverrideMaskAll; + view.ddSessionReplayPrivacyOverrides.touchPrivacy = DDTouchPrivacyLevelOverrideHide; + view.ddSessionReplayPrivacyOverrides.hide = @YES; + + // When + view.ddSessionReplayPrivacyOverrides.textAndInputPrivacy = DDTextAndInputPrivacyLevelOverrideNone; + view.ddSessionReplayPrivacyOverrides.imagePrivacy = DDImagePrivacyLevelOverrideNone; + view.ddSessionReplayPrivacyOverrides.touchPrivacy = DDTouchPrivacyLevelOverrideNone; + view.ddSessionReplayPrivacyOverrides.hide = nil; + + // Then + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.textAndInputPrivacy, DDTextAndInputPrivacyLevelOverrideNone); + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.imagePrivacy, DDImagePrivacyLevelOverrideNone); + XCTAssertEqual(view.ddSessionReplayPrivacyOverrides.touchPrivacy, DDTouchPrivacyLevelOverrideNone); + XCTAssertNil(view.ddSessionReplayPrivacyOverrides.hide); +} @end diff --git a/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift b/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift index 9c337099bf..b87370e5f3 100644 --- a/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift +++ b/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift @@ -17,8 +17,8 @@ class RUMDataModels_objcTests: XCTestCase { // Given var swiftView: RUMViewEvent = .mockRandom() - swiftView.context?.contextInfo = castAttributesToSwift(expectedContextAttributes) - swiftView.usr?.usrInfo = castAttributesToSwift(expectedUserInfoAttributes) + swiftView.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftView.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes let objcView = DDRUMViewEvent(swiftModel: swiftView) @@ -37,8 +37,8 @@ class RUMDataModels_objcTests: XCTestCase { // Given var swiftResource: RUMResourceEvent = .mockRandom() - swiftResource.context?.contextInfo = castAttributesToSwift(expectedContextAttributes) - swiftResource.usr?.usrInfo = castAttributesToSwift(expectedUserInfoAttributes) + swiftResource.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftResource.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes let objcResource = DDRUMResourceEvent(swiftModel: swiftResource) @@ -57,8 +57,8 @@ class RUMDataModels_objcTests: XCTestCase { // Given var swiftAction: RUMActionEvent = .mockRandom() - swiftAction.context?.contextInfo = castAttributesToSwift(expectedContextAttributes) - swiftAction.usr?.usrInfo = castAttributesToSwift(expectedUserInfoAttributes) + swiftAction.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftAction.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes let objcAction = DDRUMActionEvent(swiftModel: swiftAction) @@ -77,8 +77,8 @@ class RUMDataModels_objcTests: XCTestCase { // Given var swiftError: RUMErrorEvent = .mockRandom() - swiftError.context?.contextInfo = castAttributesToSwift(expectedContextAttributes) - swiftError.usr?.usrInfo = castAttributesToSwift(expectedUserInfoAttributes) + swiftError.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftError.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes let objcError = DDRUMErrorEvent(swiftModel: swiftError) @@ -97,8 +97,8 @@ class RUMDataModels_objcTests: XCTestCase { // Given var swiftLongTask: RUMLongTaskEvent = .mockRandom() - swiftLongTask.context?.contextInfo = castAttributesToSwift(expectedContextAttributes) - swiftLongTask.usr?.usrInfo = castAttributesToSwift(expectedUserInfoAttributes) + swiftLongTask.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftLongTask.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes let objcLongTask = DDRUMLongTaskEvent(swiftModel: swiftLongTask) 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/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift index b58ecc9804..4c9fccc297 100644 --- a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift +++ b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift @@ -46,6 +46,11 @@ internal class RUMSessionMatcher { sessionEventMatchers: eventMatchers ) } + .sorted { session1, session2 in + let startTime1 = session1.views.first?.viewEvents.first?.date ?? 0 + let startTime2 = session2.views.first?.viewEvents.first?.date ?? 0 + return startTime1 < startTime2 + } } // MARK: - View Visits @@ -106,6 +111,21 @@ internal class RUMSessionMatcher { let errorEventMatchers: [RUMEventMatcher] let longTaskEventMatchers: [RUMEventMatcher] + /// `RUMView` events tracked in this session. + let viewEvents: [RUMViewEvent] + + /// `RUMAction` events tracked in this session. + let actionEvents: [RUMActionEvent] + + /// `RUMResource` events tracked in this session. + let resourceEvents: [RUMResourceEvent] + + /// `RUMError` events tracked in this session. + let errorEvents: [RUMErrorEvent] + + /// `RUMLongTask` events tracked in this session. + let longTaskEvents: [RUMLongTaskEvent] + private init(applicationID: String, sessionID: String, sessionEventMatchers: [RUMEventMatcher]) throws { // Sort events so they follow increasing time order let sessionEventOrderedByTime = try sessionEventMatchers.sorted { firstEvent, secondEvent in @@ -257,6 +277,11 @@ internal class RUMSessionMatcher { } self.views = visitsEventOrderedByTime + self.viewEvents = viewEvents + self.actionEvents = actionEvents + self.resourceEvents = resourceEvents + self.errorEvents = errorEvents + self.longTaskEvents = longTaskEvents } /// Checks if this session contains a view with a specific ID. @@ -424,6 +449,15 @@ extension Array where Element == RUMSessionMatcher { } return self[0] } + + /// Returns the only two sessions in this array. + /// Throws if there are more or less than 2 sessions in this array. + func takeTwo() throws -> (RUMSessionMatcher, RUMSessionMatcher) { + guard count == 2 else { + throw RUMSessionConsistencyException(description: "Expected 2 sessions, but found \(count)") + } + return (self[0], self[1]) + } } extension Array where Element == RUMSessionMatcher.View { @@ -498,79 +532,216 @@ extension RUMSessionMatcher { // MARK: - Debugging +extension RUMSessionMatcher.View { + /// The start of this view (as timestamp; milliseconds) defined as the start timestamp of the earliest view event in this view. + var startTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } +} + extension RUMSessionMatcher: CustomStringConvertible { - var description: String { - var description = "[🎞 RUM session (application.id: \(applicationID), session.id: \(sessionID), number of views: \(views.count))]" + var description: String { renderSession() } + + /// The start of this session (as timestamp; milliseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } + + /// The start of this session (as timestamp; nanoseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampNs: Int64 { sessionStartTimestampMs * 1_000_000 } + + /// The end of this session (as timestamp; nanoseconds) defined as the end timestamp of the latest view in this session. + private var sessionEndTimestampNs: Int64 { viewEvents.map({ $0.date * 1_000_000 + $0.view.timeSpent }).max() ?? 0 } + + private func renderSession() -> String { + var output = renderBox(string: "🎞 RUM session") + output += renderAttributesBox( + attributes: [ + ("application.id", applicationID), + ("id", sessionID), + ("views.count", "\(views.count)"), + ("start", prettyDate(timestampMs: sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: sessionEndTimestampNs - sessionStartTimestampNs)), + ] + ) views.forEach { view in - description += "\n\(describe(viewVisit: view))" + output += render(view: view) } - return description + output += renderClosingLine() + return output } - private func describe(viewVisit: View) -> String { - guard let lastViewEvent = viewVisit.viewEvents.last else { - return " → [⛔️ Invalid View - it has no view events]" + private func render(view: View) -> String { + guard let lastViewEvent = view.viewEvents.last else { + return renderBox(string: "⛔️ Invalid View - it has no view events") } - var description = " → [📸 View (name: '\(viewVisit.name ?? "nil")', id: \(viewVisit.viewID), duration: \(seconds(from: lastViewEvent.view.timeSpent)) actions.count: \(lastViewEvent.view.action.count), resources.count: \(lastViewEvent.view.resource.count), errors.count: \(lastViewEvent.view.error.count), longTask.count: \(lastViewEvent.view.longTask?.count ?? 0), frozenFrames.count: \(lastViewEvent.view.frozenFrame?.count ?? 0)]" + var output = renderBox(string: "📸 RUM View (\(view.name ?? "nil"))") + output += renderAttributesBox( + attributes: [ + ("name", view.name ?? "nil"), + ("id", view.viewID), + ("date", prettyDate(timestampMs: lastViewEvent.date)), + ("date (relative in session)", pretty(milliseconds: lastViewEvent.date - sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: lastViewEvent.view.timeSpent)), + ("event counts", "view (\(view.viewEvents.count)), action (\(view.actionEvents.count)), resource (\(view.resourceEvents.count)), error (\(view.errorEvents.count)), long task (\(view.longTaskEvents.count))"), + ] + ) - if !viewVisit.actionEvents.isEmpty { - description += "\n → action events:" - description += "\n\(describe(actionEvents: viewVisit.actionEvents))" + for action in view.actionEvents { + output += renderEmptyLine() + output += render(event: action, in: view) } - if !viewVisit.resourceEvents.isEmpty { - description += "\n → resource events:" - description += "\n\(describe(resourceEvents: viewVisit.resourceEvents))" + for resource in view.resourceEvents { + output += renderEmptyLine() + output += render(event: resource, in: view) } - if !viewVisit.errorEvents.isEmpty { - description += "\n → error events:" - description += "\n\(describe(errorEvents: viewVisit.errorEvents))" + for error in view.errorEvents { + output += renderEmptyLine() + output += render(event: error, in: view) } - if !viewVisit.longTaskEvents.isEmpty { - description += "\n → long task events:" - description += "\n\(describe(longTaskEvents: viewVisit.longTaskEvents))" + for longTask in view.longTaskEvents { + output += renderEmptyLine() + output += render(event: longTask, in: view) } - return description + output += renderEmptyLine() + return output + } + + private func render(event: RUMActionEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("▶️ RUM Action", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("name", event.action.target?.name ?? "nil"), + ("type", "\(event.action.type)"), + ("loading.time", "\(event.action.loadingTime.flatMap({ pretty(nanoseconds: $0) }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMResourceEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🌎 RUM Resource", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("url", event.resource.url), + ("method", "\(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "nil")"), + ("status.code", "\(event.resource.statusCode.flatMap({ "\($0)" }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMErrorEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🧯 RUM Error", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("message", event.error.message), + ("type", event.error.type ?? "nil"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMLongTaskEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🐌 RUM Long Task", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("duration", pretty(nanoseconds: event.longTask.duration)), + ], + prefix: "→", + indentationLevel: 3 + ) + return output } - private func describe(actionEvents: [RUMActionEvent]) -> String { - return actionEvents - .map { event in - " → [▶️ Action (name: \(event.action.target?.name ?? "(null)"), type: \(event.action.type)]" - } - .joined(separator: "\n") + // MARK: - Rendering helpers + + private static let rendererWidth = 90 + + private func renderBox(string: String) -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder1 = "+" + String(repeating: "-", count: width - 2) + "+" + let horizontalBorder2 = "|" + String(repeating: "-", count: width - 2) + "|" + let visualWidth = (string as NSString).length + let padding = (width - 2 - visualWidth) / 2 + let leftPadding = String(repeating: " ", count: max(0, padding)) + let rightPadding = String(repeating: " ", count: max(0, width - 2 - visualWidth - padding)) + + let contentLine = "|\(leftPadding)\(string)\(rightPadding)|" + + return """ + \(horizontalBorder1) + \(contentLine) + \(horizontalBorder2)\n + """ } - private func describe(resourceEvents: [RUMResourceEvent]) -> String { - return resourceEvents - .map { event in - " → [🌎 Resource (url: \(event.resource.url), method: \(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "(null)"), statusCode: \(event.resource.statusCode.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderAttributesBox(attributes: [(String, String)], prefix: String = "", indentationLevel: Int = 0) -> String { + let width = RUMSessionMatcher.rendererWidth + let indentation = String(repeating: " ", count: indentationLevel) + + let contentLines = attributes.map { key, value in + let lineContent = "\(indentation)\(prefix) \(key): \(value)" + let visualWidth = (lineContent as NSString).length + let padding = max(0, width - 2 - visualWidth) + let rightPadding = String(repeating: " ", count: padding) + return "|\(lineContent)\(rightPadding)|" + } + + return """ + \(contentLines.joined(separator: "\n"))\n + """ } - private func describe(errorEvents: [RUMErrorEvent]) -> String { - return errorEvents - .map { event in - " → [🧯 Error (message: \(event.error.message), type: \(event.error.type ?? "(null)"), resource: \(event.error.resource.flatMap({ "\($0.url)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderEmptyLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "|" + String(repeating: " ", count: width - 2) + "|" + return horizontalBorder + "\n" } - private func describe(longTaskEvents: [RUMLongTaskEvent]) -> String { - return longTaskEvents - .map { event in - " → [🐌 LongTask (duration: \(seconds(from: event.longTask.duration)), isFrozenFrame: \(event.longTask.isFrozenFrame.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderClosingLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "+" + String(repeating: "-", count: width - 2) + "+" + return horizontalBorder + "\n" + } + + private func pretty(milliseconds: Int64) -> String { + pretty(nanoseconds: milliseconds * 1_000_000) } - private func seconds(from nanoseconds: Int64) -> String { - let prettySeconds = (round((Double(nanoseconds) / 1_000_000_000) * 100)) / 100 - return "\(prettySeconds)s" + private func pretty(nanoseconds: Int64) -> String { + if nanoseconds >= 1_000_000_000 { + let seconds = round((Double(nanoseconds) / 1_000_000_000) * 100) / 100 + return "\(seconds)s" + } else if nanoseconds >= 1_000_000 { + let milliseconds = round((Double(nanoseconds) / 1_000_000) * 100) / 100 + return "\(milliseconds)ms" + } else { + return "\(nanoseconds)ns" + } + } + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter + }() + + private func prettyDate(timestampMs: Int64) -> String { + let timestampSec = TimeInterval(timestampMs) / 1_000 + let date = Date(timeIntervalSince1970: timestampSec) + return RUMSessionMatcher.dateFormatter.string(from: date) } } diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec index 95c9775171..04c17f9a56 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.22.0" s.summary = "Official Datadog Crash Reporting SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogCrashReporting/Sources/CrashReporting.swift b/DatadogCrashReporting/Sources/CrashReporting.swift index e70271d724..a1dd878a07 100644 --- a/DatadogCrashReporting/Sources/CrashReporting.swift +++ b/DatadogCrashReporting/Sources/CrashReporting.swift @@ -19,12 +19,19 @@ import DatadogInternal /// /// Your crash reports appear in [Error Tracking](https://app.datadoghq.com/rum/error-tracking). public final class CrashReporting { - /// Initializes the Datadog Crash Reporter. + /// Initializes the Datadog Crash Reporter using the default + /// `PLCrashReporter` plugin. public static func enable(in core: DatadogCoreProtocol = CoreRegistry.default) { enable(with: PLCrashReporterPlugin(), in: core) } - internal static func enable(with plugin: CrashReportingPlugin, in core: DatadogCoreProtocol) { + /// Initializes the Datadog Crash Reporter with a custom Crash Reporting Plugin. + /// + /// The custom plugin will be responsible for: + /// - Provide crash report + /// - Store context data associated with crashes + /// - Provide backtraces + public static func enable(with plugin: CrashReportingPlugin, in core: DatadogCoreProtocol = CoreRegistry.default) { do { let contextProvider = CrashContextCoreProvider() @@ -38,8 +45,8 @@ public final class CrashReporting { try core.register(feature: reporter) - if let plcr = PLCrashReporterPlugin.thirdPartyCrashReporter { - try core.register(backtraceReporter: BacktraceReporter(reporter: plcr)) + if let backtraceReporter = plugin.backtraceReporter { + try core.register(backtraceReporter: backtraceReporter) } reporter.sendCrashReportIfFound() diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index fe99801329..f716cb401a 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -57,8 +57,13 @@ internal final class CrashReportingFeature: DatadogFeature { func sendCrashReportIfFound() { queue.async { self.plugin.readPendingCrashReport { [weak self] crashReport in - guard let self = self, let availableCrashReport = crashReport else { + guard let self = self else { + return false + } + + guard let availableCrashReport = crashReport else { DD.logger.debug("No pending Crash found") + self.sender.send(launch: .init(didCrash: false)) return false } @@ -67,10 +72,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/CrashReportingPlugin.swift b/DatadogCrashReporting/Sources/CrashReportingPlugin.swift index 0afde4208e..a6a3fd44bc 100644 --- a/DatadogCrashReporting/Sources/CrashReportingPlugin.swift +++ b/DatadogCrashReporting/Sources/CrashReportingPlugin.swift @@ -10,7 +10,7 @@ import DatadogInternal /// An interface for enabling crash reporting feature in Datadog SDK. /// /// The SDK calls each API on a background thread and succeeding calls are synchronized. -internal protocol CrashReportingPlugin: AnyObject { +public protocol CrashReportingPlugin: AnyObject { /// Reads unprocessed crash report if available. /// - Parameter completion: the completion block called with the value of `DDCrashReport` if a crash report is available /// or with `nil` otherwise. The value returned by the receiver should indicate if the crash report was processed correctly (`true`) @@ -18,7 +18,7 @@ internal protocol CrashReportingPlugin: AnyObject { /// /// The SDK calls this method on a background thread. The implementation is free to choice any thread /// for executing the `completion`. - func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) + func readPendingCrashReport(completion: @escaping (DDCrashReport?) -> Bool) /// Injects custom data for describing the application state in the crash report. /// This data will be attached to produced crash report and will be available in `DDCrashReport`. @@ -26,4 +26,7 @@ internal protocol CrashReportingPlugin: AnyObject { /// The SDK calls this method for each significant application state change. /// It is called on a background thread and succeeding calls are synchronized. func inject(context: Data) + + /// An instance conforming to `BacktraceReporting` capable of generating backtrace reports. + var backtraceReporter: BacktraceReporting? { get } } 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/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift index 2832b6d49c..584867807b 100644 --- a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift @@ -66,7 +66,8 @@ internal struct DDCrashReportExporter { binaryImages: formattedBinaryImages(from: crashReport), meta: formattedMeta(for: crashReport), wasTruncated: crashReport.wasTruncated, - context: crashReport.contextData + context: crashReport.contextData, + additionalAttributes: nil ) } diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift index f9c2f76646..5ac30bfeb0 100644 --- a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift @@ -6,7 +6,8 @@ import Foundation import DatadogInternal -import CrashReporter + +@preconcurrency import CrashReporter internal extension PLCrashReporterConfig { /// `PLCR` configuration used for `DatadogCrashReporting` diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift index 6d4f93a903..a59252fc50 100644 --- a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift @@ -58,4 +58,8 @@ internal class PLCrashReporterPlugin: NSObject, CrashReportingPlugin { func inject(context: Data) { PLCrashReporterPlugin.thirdPartyCrashReporter?.inject(context: context) } + + var backtraceReporter: BacktraceReporting? { + PLCrashReporterPlugin.thirdPartyCrashReporter.map { BacktraceReporter(reporter: $0) } + } } diff --git a/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift index 7f98f22381..8d7109f512 100644 --- a/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift +++ b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift @@ -8,7 +8,7 @@ import Foundation import DatadogInternal /// An interface of 3rd party crash reporter used by the DatadogCrashReporting. -internal protocol ThirdPartyCrashReporter { +internal protocol ThirdPartyCrashReporter: Sendable { /// Initializes and enables the crash reporter. init() throws diff --git a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift index 0ab2e26187..c2535470c7 100644 --- a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift +++ b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift @@ -100,12 +100,14 @@ class CrashReportingPluginTests: XCTestCase { // MARK: - Handling Errors + private let printFunction = PrintFunctionMock() + func testGivenPendingCrashReport_whenItsLoadingFails_itPrintsError() throws { let expectation = self.expectation(description: "No Crash Report was delivered to the caller.") - var errorPrinted: String? - consolePrint = { message, _ in errorPrinted = message } - defer { consolePrint = { message, _ in print(message) } } + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } let crashReporter = try ThirdPartyCrashReporterMock() let plugin = PLCrashReporterPlugin { crashReporter } @@ -126,16 +128,15 @@ class CrashReportingPluginTests: XCTestCase { waitForExpectations(timeout: 0.5, handler: nil) XCTAssertFalse(crashReporter.hasPurgedPendingCrashReport) XCTAssertEqual( - errorPrinted, + printFunction.printedMessage, "🔥 DatadogCrashReporting error: failed to load crash report: Reading error" ) } func testWhenCrashReporterCannotBeEnabled_itPrintsError() { - var errorPrinted: String? - - consolePrint = { message, _ in errorPrinted = message } - defer { consolePrint = { message, _ in print(message) } } + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } // When ThirdPartyCrashReporterMock.initializationError = ErrorMock("Initialization error") @@ -145,7 +146,7 @@ class CrashReportingPluginTests: XCTestCase { _ = PLCrashReporterPlugin { try ThirdPartyCrashReporterMock() } XCTAssertEqual( - errorPrinted, + printFunction.printedMessage, "🔥 DatadogCrashReporting error: failed to enable crash reporter: Initialization error" ) } diff --git a/DatadogCrashReporting/Tests/Mocks.swift b/DatadogCrashReporting/Tests/Mocks.swift index 32a4ffdf40..6a9d33863a 100644 --- a/DatadogCrashReporting/Tests/Mocks.swift +++ b/DatadogCrashReporting/Tests/Mocks.swift @@ -6,10 +6,9 @@ import DatadogInternal import CrashReporter - @testable import DatadogCrashReporting -internal class ThirdPartyCrashReporterMock: ThirdPartyCrashReporter { +internal final class ThirdPartyCrashReporterMock: ThirdPartyCrashReporter, @unchecked Sendable { static var initializationError: Error? var pendingCrashReport: DDCrashReport? diff --git a/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift b/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift index 840b88474b..7cd5b7f3ca 100644 --- a/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift +++ b/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift @@ -8,6 +8,7 @@ import DatadogInternal import Alamofire /// An `Alamofire.EventMonitor` which instruments `Alamofire.Session` with Datadog RUM and Tracing. +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") public class DDEventMonitor: EventMonitor { /// The instance of the SDK core notified by this monitor. private weak var core: DatadogCoreProtocol? @@ -39,6 +40,7 @@ public class DDEventMonitor: EventMonitor { } /// An `Alamofire.RequestInterceptor` which instruments `Alamofire.Session` with Datadog RUM and Tracing. +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") public class DDRequestInterceptor: RequestInterceptor { /// The instance of the SDK core notified by this monitor. private weak var core: DatadogCoreProtocol? diff --git a/DatadogExtensions/Alamofire/README.md b/DatadogExtensions/Alamofire/README.md index cb7aed5e5b..e843d3bc11 100644 --- a/DatadogExtensions/Alamofire/README.md +++ b/DatadogExtensions/Alamofire/README.md @@ -1,3 +1,9 @@ +## **Deprecated** + +**Note:** The `DatadogAlamofireExtension` pod is deprecated and will no longer be maintained. Please refer to the [Integrated Libraries][6] documentation on how to instrument Alamofire with the Datadog iOS SDK. + +--- + # Datadog Integration for Alamofire `DatadogAlamofireExtension` enables `Alamofire.Session` auto instrumentation with Datadog SDK. @@ -49,3 +55,4 @@ Pull requests are welcome. First, open an issue to discuss what you would like t [3]: https://swift.org/package-manager/ [4]: https://docs.datadoghq.com/tracing/setup_overview/setup/ios/ [5]: https://docs.datadoghq.com/real_user_monitoring/ios +[6]: https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/integrated_libraries/ios diff --git a/DatadogInternal.podspec b/DatadogInternal.podspec index 46048a1330..96a22a4b81 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.22.0" s.summary = "Datadog Internal Package. This module is not for public use." s.homepage = "https://www.datadoghq.com" @@ -17,6 +17,7 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.ios.deployment_target = '12.0' s.tvos.deployment_target = '12.0' + s.watchos.deployment_target = '7.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } diff --git a/DatadogInternal/Sources/Attributes/Attributes.swift b/DatadogInternal/Sources/Attributes/Attributes.swift index 5ee57a30f0..362374d48b 100644 --- a/DatadogInternal/Sources/Attributes/Attributes.swift +++ b/DatadogInternal/Sources/Attributes/Attributes.swift @@ -163,3 +163,39 @@ public struct LaunchArguments { /// For example, if this flag is present it can use no sampling. public static let Debug = "DD_DEBUG" } + +extension DatadogExtension where ExtendedType == [String: Any] { + public var swiftAttributes: [String: Encodable] { + type.mapValues { AnyEncodable($0) } + } +} + +extension DatadogExtension where ExtendedType == [String: Encodable] { + public var objCAttributes: [String: Any] { + type.compactMapValues { ($0 as? AnyEncodable)?.value } + } +} + +extension AttributeValue { + /// Instance Datadog extension point. + /// + /// `AttributeValue` aka `Encodable` is a protocol and cannot be extended + /// with conformance to`DatadogExtension`, so we need to define the `dd` + /// endpoint. + public var dd: DatadogExtension { + DatadogExtension(self) + } +} + +extension DatadogExtension where ExtendedType == AttributeValue { + public func decode(_: T.Type = T.self) -> T? { + switch type { + case let encodable as _AnyEncodable: + return encodable.value as? T + case let val as T: + return val + default: + return nil + } + } +} diff --git a/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift index f0ee4e5288..ea5af300a9 100644 --- a/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift +++ b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift @@ -17,7 +17,7 @@ public extension Thread { } /// A protocol for types capable of generating backtrace reports. -public protocol BacktraceReporting { +public protocol BacktraceReporting: Sendable { /// Generates a backtrace report for given thread ID. /// /// The thread given by `threadID` will be promoted in the main stack of returned `BacktraceReport` (`report.stack`). @@ -41,7 +41,7 @@ public extension BacktraceReporting { } } -internal struct CoreBacktraceReporter: BacktraceReporting { +internal struct CoreBacktraceReporter: BacktraceReporting, @unchecked Sendable { /// A weak core reference. private weak var core: DatadogCoreProtocol? diff --git a/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift b/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift new file mode 100644 index 0000000000..d9149f8889 --- /dev/null +++ b/DatadogInternal/Sources/Benchmarks/BenchmarkProfiler.swift @@ -0,0 +1,63 @@ +/* + * 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 DD_BENCHMARK +/// The profiler endpoint to collect data for benchmarking. +public var profiler: BenchmarkProfiler = NOPBenchmarkProfiler() +#else +/// The profiler endpoint to collect data for benchmarking. This static variable can only +/// be mutated in the benchmark environment. +public let profiler: BenchmarkProfiler = NOPBenchmarkProfiler() +#endif + +/// The Benchmark Profiler provides interfaces to collect data in a benchmark +/// environment. +/// +/// During benchmarking, a concrete implementation of the profiler will be +/// injected to collect data during execution of the SDK. +/// +/// In production, the profiler is no-op and immutable. +public protocol BenchmarkProfiler { + /// Returns a `BenchmarkTracer` instance for the given operation. + /// + /// The profiler must return the same instance of a tracer for the same operation. + /// + /// - Parameter operation: The tracer operation name. The parameter is an auto-closure + /// to not intialise the value if the profiler is no-op. + /// - Returns: The tracer instance. + func tracer(operation: @autoclosure () -> String) -> BenchmarkTracer +} + +/// The Benchmark Tracer will create and start spans in a benchmark environment. +/// This tracer can be used to measure CPU Time of inner operation of the SDK. +/// In production, the Benchmark Tracer is no-op. +public protocol BenchmarkTracer { + /// Creates and starts a span at the current time. + /// + /// The span will be activated automatically and linked to its parent in this tracer context. + /// + /// - Parameter named: The span name. The parameter is an auto-closure + /// to not intialise the value if the profiler is no-op. + /// - Returns: The started span. + func startSpan(named: @autoclosure () -> String) -> BenchmarkSpan +} + +/// A timespan of an operation in a benchmark environment. +public protocol BenchmarkSpan { + /// Stops the span at the current time. + func stop() +} + +private final class NOPBenchmarkProfiler: BenchmarkProfiler, BenchmarkTracer, BenchmarkSpan { + /// no-op + func tracer(operation: @autoclosure () -> String) -> BenchmarkTracer { self } + /// no-op + func startSpan(named: @autoclosure () -> String) -> BenchmarkSpan { self } + /// no-op + func stop() {} +} diff --git a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift index 5093712c7f..78586b83c5 100644 --- a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift +++ b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift @@ -9,8 +9,8 @@ import Foundation /// A property wrapper using a fair, POSIX conforming reader-writer lock for atomic /// access to the value. It is optimised for concurrent reads and exclusive writes. /// -/// The wrapper is a class to prevent copying the lock, it creates and initilaizes a `pthread_rwlock_t`. -/// An additional method `mutate` allow to safely mutate the value in-place (to read it +/// The wrapper is a class to prevent copying the lock, it creates and initializes a `pthread_rwlock_t`. +/// An additional method `mutate` allows to safely mutate the value in-place (to read it /// and write it while obtaining the lock only once). @propertyWrapper public final class ReadWriteLock: @unchecked Sendable { diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift index f3a57a0236..271d257eab 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -6,6 +6,15 @@ import Foundation +/// A protocol that provides access to the current application state. +/// See: https://developer.apple.com/documentation/uikit/uiapplication/state +public protocol AppStateProvider: Sendable { + /// The current application state. + /// + /// **Note**: Must be called on the main thread. + var current: AppState { get } +} + /// Application state. public enum AppState: Codable, PassthroughAnyCodable { /// The app is running in the foreground and currently receiving events. @@ -15,13 +24,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 } } @@ -140,23 +151,66 @@ extension AppStateHistory { } } -#if canImport(UIKit) +#if canImport(WatchKit) + +import WatchKit + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let wkState = WKExtension.dd.shared.applicationState + return AppState(wkState) + } +} + +extension AppState { + public init(_ state: WKApplicationState) { + switch state { + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background + @unknown default: + self = .active // in case a new state is introduced, default to most expected state + } + } +} + +#elseif canImport(UIKit) import UIKit +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state + return AppState(uiKitState) + } +} + extension AppState { public init(_ state: UIApplication.State) { switch state { - case .active: - self = .active - case .inactive: - self = .inactive - case .background: - self = .background - @unknown default: - self = .active // in case a new state is introduced, we rather want to fallback to most expected state + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background + @unknown default: self = .active // in case a new state is introduced, default to most expected state } } } +#else // macOS (no UIKit and no WatchKit) + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + public let current: AppState = .active +} + #endif diff --git a/DatadogInternal/Sources/Context/ApplicationNotifications.swift b/DatadogInternal/Sources/Context/ApplicationNotifications.swift new file mode 100644 index 0000000000..43af3abf8a --- /dev/null +++ b/DatadogInternal/Sources/Context/ApplicationNotifications.swift @@ -0,0 +1,49 @@ +/* + * 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 canImport(UIKit) +import UIKit +#if canImport(WatchKit) +import WatchKit +#endif + +/// Convenient wrapper to get system notifications independent from platform +public enum ApplicationNotifications { + public static var didBecomeActive: Notification.Name { + #if canImport(WatchKit) + WKExtension.applicationDidBecomeActiveNotification + #else + UIApplication.didBecomeActiveNotification + #endif + } + + public static var willResignActive: Notification.Name { + #if canImport(WatchKit) + WKExtension.applicationWillResignActiveNotification + #else + UIApplication.willResignActiveNotification + #endif + } + + public static var didEnterBackground: Notification.Name { + #if canImport(WatchKit) + WKExtension.applicationDidEnterBackgroundNotification + #else + UIApplication.didEnterBackgroundNotification + #endif + } + + public static var willEnterForeground: Notification.Name { + #if canImport(WatchKit) + WKExtension.applicationWillEnterForegroundNotification + #else + UIApplication.willEnterForegroundNotification + #endif + } +} +#endif 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/DateProvider.swift b/DatadogInternal/Sources/Context/DateProvider.swift index 38c3c9be79..ac17720e37 100644 --- a/DatadogInternal/Sources/Context/DateProvider.swift +++ b/DatadogInternal/Sources/Context/DateProvider.swift @@ -7,7 +7,7 @@ import Foundation /// Provides current device time information. -public protocol DateProvider { +public protocol DateProvider: Sendable { /// Current device time. /// /// A specific point in time, independent of any calendar or time zone. diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index 99b2f885b5..9dd865b880 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -8,6 +8,15 @@ import Foundation /// Describes current device information. public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { + /// Represents the type of device. + public enum DeviceType: Codable, Equatable, PassthroughAnyCodable { + case iPhone + case iPod + case iPad + case appleTV + case other(modelName: String, osName: String) + } + // MARK: - Info /// Device manufacturer name. Always'Apple' @@ -19,33 +28,84 @@ public struct DeviceInfo: Codable, Equatable, PassthroughAnyCodable { /// Device model name, e.g. "iPhone10,1", "iPhone13,2". public let model: String + /// The type of device. + public let type: DeviceType + /// The name of operating system, e.g. "iOS", "iPadOS", "tvOS". public let osName: String /// The version of the operating system, e.g. "15.4.1". public let osVersion: String + /// The major version of the operating system, e.g. "15". + public let osVersionMajor: String + /// The build numer of the operating system, e.g. "15D21" or "13D20". public let osBuildNumber: String? /// 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 self.model = model + self.type = DeviceType(modelName: model, osName: osName) self.osName = osName self.osVersion = osVersion + self.osVersionMajor = osVersion.split(separator: ".").first.map { String($0) } ?? osVersion self.osBuildNumber = osBuildNumber self.architecture = architecture + self.isSimulator = isSimulator + self.vendorId = vendorId + self.isDebugging = isDebugging + self.systemBootTime = systemBootTime + } +} + +private extension DeviceInfo.DeviceType { + /// Infers `DeviceType` from provided model name and operating system name. + /// - Parameters: + /// - modelName: The name of the device model, e.g. "iPhone10,1". + /// - osName: The name of the operating system, e.g. "iOS", "tvOS". + init(modelName: String, osName: String) { + let lowercasedModelName = modelName.lowercased() + let lowercasedOSName = osName.lowercased() + + if lowercasedModelName.hasPrefix("iphone") { + self = .iPhone + } else if lowercasedModelName.hasPrefix("ipod") { + self = .iPod + } else if lowercasedModelName.hasPrefix("ipad") { + self = .iPad + } else if lowercasedModelName.hasPrefix("appletv") || lowercasedOSName == "tvos" { + self = .appleTV + } else { + self = .other(modelName: modelName, osName: osName) + } } } @@ -55,43 +115,54 @@ import MachO import UIKit extension DeviceInfo { - /// Creates device info based on UIKit description. + /// Creates device info based on device description. /// /// - Parameters: /// - processInfo: The current process information. - /// - device: The `UIDevice` description. + /// - device: The device description. public init( - processInfo: ProcessInfo = .processInfo, - device: UIDevice = .current + processInfo: ProcessInfo, + device: _UIDevice = .dd.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() - // Real iOS device + let model = try? sysctl.model() + // Real device self.init( name: device.model, model: model ?? device.model, 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 - // iOS Simulator - battery monitoring doesn't work on Simulator, so return "always OK" value + // Simulator - battery monitoring doesn't work on Simulator, so return "always OK" value self.init( name: device.model, model: "\(model) Simulator", 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 +174,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,8 +199,34 @@ 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 ) } } #endif + +#if canImport(WatchKit) +import WatchKit + +public typealias _UIDevice = WKInterfaceDevice + +extension _UIDevice: DatadogExtended {} +extension DatadogExtension where ExtendedType == _UIDevice { + /// Returns the shared device object. + public static var current: ExtendedType { .current() } +} +#elseif canImport(UIKit) +import UIKit + +public typealias _UIDevice = UIDevice + +extension _UIDevice: DatadogExtended {} +extension DatadogExtension where ExtendedType == _UIDevice { + /// Returns the shared device object. + public static var current: ExtendedType { .current } +} +#endif 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/CoreRegistry.swift b/DatadogInternal/Sources/CoreRegistry.swift index f3c917038a..54d0b771e6 100644 --- a/DatadogInternal/Sources/CoreRegistry.swift +++ b/DatadogInternal/Sources/CoreRegistry.swift @@ -76,4 +76,17 @@ public final class CoreRegistry { public static func instance(named name: String) -> DatadogCoreProtocol { instances[name] ?? NOPDatadogCore() } + + /// Checks if the specified `DatadogFeature` is enabled for any registered core instance. + /// + /// - Parameter feature: The feature type to check for. + /// - Returns: `true` if the feature is enabled in at least one instance, otherwise `false`. + public static func isFeatureEnabled(feature: T.Type) -> Bool where T: DatadogFeature { + for instance in instances.values { + if instance.get(feature: T.self) != nil { + return true + } + } + return false + } } diff --git a/DatadogInternal/Sources/DD.swift b/DatadogInternal/Sources/DD.swift index fd10e21633..67262e3f4f 100644 --- a/DatadogInternal/Sources/DD.swift +++ b/DatadogInternal/Sources/DD.swift @@ -30,7 +30,7 @@ import OSLog #endif /// Function printing `String` content to console. -public var consolePrint: (String, CoreLoggerLevel) -> Void = { message, level in +public var consolePrint: @Sendable (String, CoreLoggerLevel) -> Void = { message, level in #if canImport(OSLog) if #available(iOS 14.0, tvOS 14.0, *) { switch level { diff --git a/DatadogInternal/Sources/DataStore/DataStore.swift b/DatadogInternal/Sources/DataStore/DataStore.swift index 4d9b170d9f..e437ac639b 100644 --- a/DatadogInternal/Sources/DataStore/DataStore.swift +++ b/DatadogInternal/Sources/DataStore/DataStore.swift @@ -61,6 +61,12 @@ public protocol DataStore { /// /// - Parameter key: The unique identifier for the value to be deleted. Must be a valid file name, as it will be persisted in files. func removeValue(forKey key: String) + + /// Clears all data that has not already yet been uploaded Datadog servers. + /// + /// Note: This may impact the SDK's ability to detect App Hangs and Watchdog Terminations + /// or other features that rely on data persisted in the data store. + func clearAllData() } public extension DataStore { @@ -83,4 +89,6 @@ public struct NOPDataStore: DataStore { public func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) {} /// no-op public func removeValue(forKey key: String) {} + /// no-op + public func clearAllData() {} } diff --git a/DatadogInternal/Sources/DatadogCoreProtocol.swift b/DatadogInternal/Sources/DatadogCoreProtocol.swift index c9655e8192..486ad1ab97 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`. @@ -223,7 +222,7 @@ extension BaggageSharing { } /// Feature scope provides a context and a writer to build a record event. -public protocol FeatureScope: MessageSending, BaggageSharing { +public protocol FeatureScope: MessageSending, BaggageSharing, Sendable { /// Retrieve the core context and event writer. /// /// The Feature scope provides the current Datadog context and event writer for building and recording events. @@ -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/Extensions/Data+Crypto.swift b/DatadogInternal/Sources/Extensions/Data+Crypto.swift new file mode 100644 index 0000000000..a4a2d81713 --- /dev/null +++ b/DatadogInternal/Sources/Extensions/Data+Crypto.swift @@ -0,0 +1,20 @@ +/* + * 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 CommonCrypto + +extension Data { + public func sha1() -> String { + let hash = withUnsafeBytes { bytes -> [UInt8] in + var hash: [UInt8] = Array(repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) + CC_SHA1(bytes.baseAddress, CC_LONG(count), &hash) + return hash + } + + return hash.map { String(format: "%02x", $0) }.joined() + } +} diff --git a/DatadogInternal/Sources/Extensions/DatadogExtended.swift b/DatadogInternal/Sources/Extensions/DatadogExtended.swift index 4b840bd452..7d37267226 100644 --- a/DatadogInternal/Sources/Extensions/DatadogExtended.swift +++ b/DatadogInternal/Sources/Extensions/DatadogExtended.swift @@ -46,3 +46,6 @@ extension DatadogExtended { set {} } } + +extension Array: DatadogExtended {} +extension Dictionary: DatadogExtended {} 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/DDCrashReport.swift b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift index 71cd46a0f6..b3bd9a90ff 100644 --- a/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift +++ b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift @@ -76,6 +76,8 @@ public struct DDCrashReport: Codable, PassthroughAnyCodable { public let wasTruncated: Bool /// The last context injected through `inject(context:)` public let context: Data? + /// Addtional attributes of the crash + public let additionalAttributes: AnyCodable public init( date: Date?, @@ -86,7 +88,8 @@ public struct DDCrashReport: Codable, PassthroughAnyCodable { binaryImages: [BinaryImage], meta: Meta, wasTruncated: Bool, - context: Data? + context: Data?, + additionalAttributes: [String: Encodable]? ) { self.date = date self.type = type @@ -97,5 +100,6 @@ public struct DDCrashReport: Codable, PassthroughAnyCodable { self.meta = meta self.wasTruncated = wasTruncated self.context = context + self.additionalAttributes = AnyCodable(additionalAttributes) } } 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/Models/SessionReplay/SessionReplayConfiguration.swift b/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift index 1cf6f3814d..5dc8ef3a56 100644 --- a/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift +++ b/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift @@ -8,6 +8,8 @@ import Foundation public let SessionReplayFeaturneName = "session-replay" +// MARK: Deprecated Global Privacy Level + /// Available privacy levels for content masking in Session Replay. public enum SessionReplayPrivacyLevel: String { /// Record all content. @@ -17,9 +19,46 @@ public enum SessionReplayPrivacyLevel: String { case mask /// Mask input elements, but record all other content. - case maskUserInput = "mask_user_input" + case maskUserInput = "mask-user-input" +} + +// MARK: Fine-Grained Privacy Levels + +/// Available privacy levels for text and input masking in Session Replay. +public enum TextAndInputPrivacyLevel: String, CaseIterable { + /// Show all texts except sensitive inputs, eg. password fields. + case maskSensitiveInputs = "mask_sensitive_inputs" + + /// Mask all inputs fields, eg. textfields, switches, checkboxes. + case maskAllInputs = "mask_all_inputs" + + /// Mask all texts and inputs, eg. labels. + case maskAll = "mask_all" +} + +/// Available privacy levels for image masking in the Session Replay. +public enum ImagePrivacyLevel: String { + /// Only SF Symbols and images loaded using UIImage(named:) that are bundled within the application will be recorded. + case maskNonBundledOnly = "mask_non_bundled_only" + + /// No images will be recorded. + case maskAll = "mask_all" + + /// All images will be recorded, including the ones downloaded from the Internet or generated during the app runtime. + case maskNone = "mask_none" } +/// Available privacy levels for touch masking in Session Replay. +public enum TouchPrivacyLevel: String { + /// Show all user touches. + case show + + /// Hide all user touches. + case hide +} + +// MARK: SessionReplayConfiguration + /// The Session Replay shared configuration. /// /// The Feature object named `session-replay` will be registered to the core @@ -32,8 +71,10 @@ public enum SessionReplayPrivacyLevel: String { /// ) /// public protocol SessionReplayConfiguration { - /// The privacy level to use for the web view replay recording. - var privacyLevel: SessionReplayPrivacyLevel { get } + /// Fine-Grained privacy levels to use in Session Replay. + var textAndInputPrivacyLevel: TextAndInputPrivacyLevel { get } + var imagePrivacyLevel: ImagePrivacyLevel { get } + var touchPrivacyLevel: TouchPrivacyLevel { get } } extension DatadogFeature where Self: SessionReplayConfiguration { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift index b890b32ceb..b134a4683c 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift @@ -7,7 +7,7 @@ import Foundation /// Defines whether the trace context should be injected into all requests or only sampled ones. -public enum TraceContextInjection { +public enum TraceContextInjection: CaseIterable { /// Injects trace context into all requests irrespective of the sampling decision. case all diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift index 25c5da94d8..84ecd506f0 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift @@ -195,12 +195,11 @@ public protocol TraceIDGenerator { func generate() -> TraceID } -/// A Default `TraceID` genarator. +/// A Default `TraceID` generator. +/// TraceId are 128 bit and follows a specific format: +/// <32-bit unix seconds> <32 bits of zero> <64 random bits> public struct DefaultTraceIDGenerator: TraceIDGenerator { - /// Describes the lower and upper boundary of tracing ID generation. - /// - /// * Lower: starts with `1` as `0` is reserved for historical reason: 0 == "unset", ref: dd-trace-java:DDId.java. - /// * Upper: equals to `2 ^ 63 - 1` as some tracers can't handle the `2 ^ 64 -1` range, ref: dd-trace-java:DDId.java. + /// Describes the lower and upper boundary of lower part of the trace ID. public static let defaultGenerationRange = (1...UInt64.max) /// The generator's range. diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift index 1429c1a504..6d02211757 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift @@ -25,7 +25,7 @@ public enum TraceSamplingStrategy { case .headBased: return DeterministicSampler(shouldSample: traceContext.isKept, samplingRate: traceContext.sampleRate) case .custom(let sampleRate): - return Sampler(samplingRate: sampleRate) + return DeterministicSampler(baseId: traceContext.traceID.idLo, samplingRate: sampleRate) } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift index 66c33ba24e..83c1ce819e 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift @@ -34,7 +34,7 @@ public struct URLSessionInterceptor { /// This is to bridge the gap between what is encoded into HTTP headers and what is later needed for processing /// the interception (unlike request headers, the `TraceContext` holds the original information on trace sampling). @ReadWriteLock - private var contextsByTraceID: [TraceID: [TraceContext]] = [:] + static var contextsByTraceID: [TraceID: [TraceContext]] = [:] /// Tells the interceptor to modify a URL request. /// @@ -45,7 +45,7 @@ public struct URLSessionInterceptor { public func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts? = nil) -> URLRequest { let (request, traceContexts) = feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) if let traceID = extractTraceID(from: request) { - contextsByTraceID[traceID] = traceContexts + URLSessionInterceptor.contextsByTraceID[traceID] = traceContexts } return request } @@ -58,7 +58,7 @@ public struct URLSessionInterceptor { public func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts? = nil) { var injectedTraceContexts: [TraceContext] = [] if let request = task.currentRequest, let traceID = extractTraceID(from: request) { - injectedTraceContexts = contextsByTraceID[traceID] ?? [] + injectedTraceContexts = URLSessionInterceptor.contextsByTraceID[traceID] ?? [] } feature.intercept(task: task, with: injectedTraceContexts, additionalFirstPartyHosts: additionalFirstPartyHosts) @@ -91,7 +91,7 @@ public struct URLSessionInterceptor { feature.task(task, didCompleteWithError: error) if let request = task.currentRequest, let traceID = extractTraceID(from: request) { - contextsByTraceID[traceID] = nil + URLSessionInterceptor.contextsByTraceID[traceID] = nil } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift index da1a44f42c..71b0c66f0e 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift @@ -21,7 +21,7 @@ extension DatadogExtension where ExtendedType: URLSessionTask { /// Returns the delegate instance the task is reporting to. var delegate: URLSessionDelegate? { - if #available(iOS 15.0, tvOS 15.0, *), let delegate = type.delegate { + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *), let delegate = type.delegate { return delegate } diff --git a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift index 5651cfa1c9..1e5bf4ff85 100644 --- a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift +++ b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift @@ -10,6 +10,8 @@ import Foundation public enum SDKMetricFields { /// Metric type key. It expects `String` value. public static let typeKey = "metric_type" + /// The first sample rate applied to the metric. + public static let headSampleRate = "head_sample_rate" /// Key referencing the session ID (`String`) that the metric should be sent with. It expects `String` value. /// 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..1b9cb195a0 100644 --- a/DatadogInternal/Sources/Telemetry/Telemetry.swift +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -6,6 +6,7 @@ import Foundation +/// Defines the type of configuration telemetry events supported by the SDK. public struct ConfigurationTelemetry: Equatable { public let actionNameAttribute: String? public let allowFallbackToLocalStorage: Bool? @@ -16,8 +17,11 @@ public struct ConfigurationTelemetry: Equatable { public let batchSize: Int64? public let batchUploadFrequency: Int64? public let dartVersion: String? - public let defaultPrivacyLevel: String? public let forwardErrorsToLogs: Bool? + public let defaultPrivacyLevel: String? + public let textAndInputPrivacyLevel: String? + public let imagePrivacyLevel: String? + public let touchPrivacyLevel: String? public let initializationType: String? public let mobileVitalsUpdatePeriod: Int64? public let reactNativeVersion: String? @@ -25,7 +29,7 @@ public struct ConfigurationTelemetry: Equatable { public let sessionReplaySampleRate: Int64? public let sessionSampleRate: Int64? public let silentMultipleInit: Bool? - public let startSessionReplayRecordingManually: Bool? + public let startRecordingImmediately: Bool? public let telemetryConfigurationSampleRate: Int64? public let telemetrySampleRate: Int64? public let tracerAPI: String? @@ -57,11 +61,105 @@ public struct ConfigurationTelemetry: Equatable { public let useWorkerUrl: Bool? } +/// A telemetry event that can be sampled in addition to the global telemetry sample rate. +public protocol SampledTelemetry { + /// The sample rate for this metric, applied in addition to the telemetry sample rate. + var sampleRate: SampleRate { get } +} + +public struct MetricTelemetry: SampledTelemetry { + /// The default sample rate for metric events (15%), applied in addition to the telemetry sample rate (20% by default). + public static let defaultSampleRate: SampleRate = 15 + + /// The name of the metric. + public let name: String + + /// The attributes associated with this metric. + public let attributes: [String: Encodable] + + /// The sample rate for this metric, applied in addition to the telemetry sample rate. + /// + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public let sampleRate: SampleRate +} + +/// Describes the type of the usage telemetry events supported by the SDK. +public struct UsageTelemetry: SampledTelemetry { + /// Supported usage telemetry events. + public enum Event { + /// setTrackingConsent API + case setTrackingConsent(TrackingConsent) + /// stopSession API + case stopSession + /// startView API + case startView + /// addAction API + case addAction + /// addError API + case addError + /// setGlobalContext, setGlobalContextProperty, addAttribute APIs + case setGlobalContext + /// setUser, setUserProperty, setUserInfo APIs + case setUser + /// addFeatureFlagEvaluation API + case addFeatureFlagEvaluation + /// addFeatureFlagEvaluation API + case addViewLoadingTime(ViewLoadingTime) + + /// Describes the properties of `addViewLoadingTime` usage telemetry. + public struct ViewLoadingTime { + /// Whether the available view is not active + public let noActiveView: Bool + /// Whether the view is not available + public let noView: Bool + /// Whether the loading time was overwritten + public let overwritten: Bool + + public init(noActiveView: Bool, noView: Bool, overwritten: Bool) { + self.noActiveView = noActiveView + self.noView = noView + self.overwritten = overwritten + } + } + } + + /// The default sample rate for usage telemetry events (15%), applied in addition to the telemetry sample rate (20% by default). + public static let defaultSampleRate: SampleRate = 15 + + /// The usage telemetry event. + public let event: Event + + /// The sample rate for usage event, applied in addition to the telemetry sample rate. + /// + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this event's sample rate is 15%, the effective sample rate for this event will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the event has been processed by the SDK core (tail-based sampling). + public let sampleRate: SampleRate + + public init(event: Event, sampleRate: SampleRate = Self.defaultSampleRate) { + self.event = event + self.sampleRate = sampleRate + } +} + +/// Defines different types of telemetry messages supported by the SDK. public enum TelemetryMessage { + /// A debug log message. case debug(id: String, message: String, attributes: [String: Encodable]?) + /// An execution error. case error(id: String, message: String, kind: String, stack: String) + /// A configuration telemetry. case configuration(ConfigurationTelemetry) - case metric(name: String, attributes: [String: Encodable]) + case metric(MetricTelemetry) + case usage(UsageTelemetry) } /// The `Telemetry` protocol defines methods to collect debug information @@ -74,38 +172,71 @@ public protocol Telemetry { } public extension Telemetry { - /// Starts a method call. + /// Starts timing a method call using the "Method Called" metric. /// /// - Parameters: - /// - operationName: Platform agnostic name of the operation. - /// - callerClass: The name of the class that calls the method. - /// - samplingRate: The sampling rate of the method call. Value between `0.0` and `100.0`, where `0.0` means NO event will be processed and `100.0` means ALL events will be processed. Note that this value is multiplicated by telemetry sampling (by default 20%) and metric events sampling (hardcoded to 15%). Making it effectively 3% sampling rate for sending events, when this value is set to `100`. + /// - operationName: A platform-agnostic name for the operation. + /// - callerClass: The name of the class that invokes the method. + /// - headSampleRate: The sample rate for **head-based** sampling of the method call metric. Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: The head sample rate is compounded with the tail sample rate, which is configured in `stopMethodCalled()`. Both are applied + /// in addition to the telemetry sample rate. For example, if the telemetry sample rate is 20% (default), the head sample rate is 1%, and the tail sample + /// rate is 15% (default), the effective sample rate will be 20% x 1% x 15% = 0.03%. /// - /// - Returns: A `MethodCalledTrace` instance to be used to stop the method call and measure it's execution time. It can be `nil` if the method call is not sampled. + /// Unlike the telemetry sample rate and tail-based sampling in `stopMethodCalled()`, this sample rate is applied at the start of the method call timing. + /// This head-based sampling reduces the impact of processing high-frequency metrics in the SDK core, as most samples can be dropped + /// before being passed to the message bus. + /// + /// - Returns: A `MethodCalledTrace` instance for stopping the method call and measuring its execution time, or `nil` if the method call is not sampled. func startMethodCalled( operationName: String, callerClass: String, - samplingRate: Float = 100.0 + headSampleRate: SampleRate ) -> MethodCalledTrace? { - if Sampler(samplingRate: samplingRate).sample() { + if Sampler(samplingRate: headSampleRate).sample() { return MethodCalledTrace( operationName: operationName, - callerClass: callerClass + callerClass: callerClass, + headSampleRate: headSampleRate ) } else { return nil } } - /// Stops a method call, transforms method call metric to telemetry message, - /// and transmits on the message-bus of the core. + /// Stops timing a method call and posts a value for the "Method Called" metric. + /// + /// This method applies tail-based sampling in addition to the head-based sampling applied in `startMethodCalled()`. + /// The tail sample rate is compounded with the head sample rate and the telemetry sample rate to determine the effective sample rate. /// - /// - Parameters + /// - Parameters: /// - metric: The `MethodCalledTrace` instance. - /// - isSuccessful: A flag indicating if the method call was successful. - func stopMethodCalled(_ metric: MethodCalledTrace?, isSuccessful: Bool = true) { + /// - isSuccessful: A flag indicating whether the method call was successful. + /// - tailSampleRate: The sample rate for **tail-based** sampling of the metric, applied in telemetry receiver after the metric is processed by the SDK core. + /// Defaults to `MetricTelemetry.defaultSampleRate` (15%). + func stopMethodCalled( + _ metric: MethodCalledTrace?, + isSuccessful: Bool = true, + tailSampleRate: SampleRate = MetricTelemetry.defaultSampleRate + ) { if let metric = metric { - send(telemetry: metric.asTelemetryMetric(isSuccessful: isSuccessful)) + let executionTime = -metric.startTime.timeIntervalSinceNow.toInt64Nanoseconds + send( + telemetry: .metric( + MetricTelemetry( + name: MethodCalledMetric.name, + attributes: [ + MethodCalledMetric.executionTime: executionTime, + MethodCalledMetric.operationName: metric.operationName, + MethodCalledMetric.callerClass: metric.callerClass, + MethodCalledMetric.isSuccessful: isSuccessful, + SDKMetricFields.headSampleRate: metric.headSampleRate, + SDKMetricFields.typeKey: MethodCalledMetric.typeValue + ], + sampleRate: tailSampleRate + ) + ) + ) } } } @@ -114,24 +245,8 @@ public extension Telemetry { public struct MethodCalledTrace { let operationName: String let callerClass: String + let headSampleRate: SampleRate let startTime = Date() - - var exectutionTime: Int64 { - return -startTime.timeIntervalSinceNow.toInt64Nanoseconds - } - - func asTelemetryMetric(isSuccessful: Bool) -> TelemetryMessage { - return .metric( - name: MethodCalledMetric.name, - attributes: [ - MethodCalledMetric.executionTime: exectutionTime, - MethodCalledMetric.operationName: operationName, - MethodCalledMetric.callerClass: callerClass, - MethodCalledMetric.isSuccessful: isSuccessful, - SDKMetricFields.typeKey: MethodCalledMetric.typeValue - ] - ) - } } extension Telemetry { @@ -250,8 +365,11 @@ extension Telemetry { batchSize: Int64? = nil, batchUploadFrequency: Int64? = nil, dartVersion: String? = nil, - defaultPrivacyLevel: String? = nil, forwardErrorsToLogs: Bool? = nil, + defaultPrivacyLevel: String? = nil, + textAndInputPrivacyLevel: String? = nil, + imagePrivacyLevel: String? = nil, + touchPrivacyLevel: String? = nil, initializationType: String? = nil, mobileVitalsUpdatePeriod: Int64? = nil, reactNativeVersion: String? = nil, @@ -259,7 +377,7 @@ extension Telemetry { sessionReplaySampleRate: Int64? = nil, sessionSampleRate: Int64? = nil, silentMultipleInit: Bool? = nil, - startSessionReplayRecordingManually: Bool? = nil, + startRecordingImmediately: Bool? = nil, telemetryConfigurationSampleRate: Int64? = nil, telemetrySampleRate: Int64? = nil, tracerAPI: String? = nil, @@ -300,8 +418,11 @@ extension Telemetry { batchSize: batchSize, batchUploadFrequency: batchUploadFrequency, dartVersion: dartVersion, - defaultPrivacyLevel: defaultPrivacyLevel, forwardErrorsToLogs: forwardErrorsToLogs, + defaultPrivacyLevel: defaultPrivacyLevel, + textAndInputPrivacyLevel: textAndInputPrivacyLevel, + imagePrivacyLevel: imagePrivacyLevel, + touchPrivacyLevel: touchPrivacyLevel, initializationType: initializationType, mobileVitalsUpdatePeriod: mobileVitalsUpdatePeriod, reactNativeVersion: reactNativeVersion, @@ -309,7 +430,7 @@ extension Telemetry { sessionReplaySampleRate: sessionReplaySampleRate, sessionSampleRate: sessionSampleRate, silentMultipleInit: silentMultipleInit, - startSessionReplayRecordingManually: startSessionReplayRecordingManually, + startRecordingImmediately: startRecordingImmediately, telemetryConfigurationSampleRate: telemetryConfigurationSampleRate, telemetrySampleRate: telemetrySampleRate, tracerAPI: tracerAPI, @@ -342,16 +463,23 @@ extension Telemetry { )) } - /// Collect metric value. + /// Collects a metric value. /// - /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicates filtering and - /// are get sampled with a different rate. Metric attributes are used to create facets for later querying and graphing. + /// Metrics are reported as debug telemetry. Unlike regular events, they are not subject to duplicate filtering and + /// are sampled at a different rate. Metric attributes are used to create facets for later querying and graphing. /// /// - Parameters: - /// - name: The name of this metric. - /// - attributes: Parameters associated with this metric. - public func metric(name: String, attributes: [String: Encodable]) { - send(telemetry: .metric(name: name, attributes: attributes)) + /// - name: The name of the metric. + /// - attributes: The attributes associated with this metric. + /// - sampleRate: The sample rate for this metric, applied in addition to the telemetry sample rate (15% by default). + /// Must be a value between `0` (reject all) and `100` (keep all). + /// + /// Note: This sample rate is compounded with the telemetry sample rate. For example, if the telemetry sample rate is 20% (default) + /// and this metric's sample rate is 15%, the effective sample rate for this metric will be 3%. + /// + /// This sample rate is applied in the telemetry receiver, after the metric has been processed by the SDK core (tail-based sampling). + public func metric(name: String, attributes: [String: Encodable], sampleRate: SampleRate = MetricTelemetry.defaultSampleRate) { + send(telemetry: .metric(MetricTelemetry(name: name, attributes: attributes, sampleRate: sampleRate))) } } @@ -395,6 +523,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( @@ -407,8 +541,11 @@ extension ConfigurationTelemetry { batchSize: other.batchSize ?? batchSize, batchUploadFrequency: other.batchUploadFrequency ?? batchUploadFrequency, dartVersion: other.dartVersion ?? dartVersion, - defaultPrivacyLevel: other.defaultPrivacyLevel ?? defaultPrivacyLevel, forwardErrorsToLogs: other.forwardErrorsToLogs ?? forwardErrorsToLogs, + defaultPrivacyLevel: other.defaultPrivacyLevel ?? defaultPrivacyLevel, + textAndInputPrivacyLevel: other.textAndInputPrivacyLevel ?? textAndInputPrivacyLevel, + imagePrivacyLevel: other.imagePrivacyLevel ?? imagePrivacyLevel, + touchPrivacyLevel: other.touchPrivacyLevel ?? touchPrivacyLevel, initializationType: other.initializationType ?? initializationType, mobileVitalsUpdatePeriod: other.mobileVitalsUpdatePeriod ?? mobileVitalsUpdatePeriod, reactNativeVersion: other.reactNativeVersion ?? reactNativeVersion, @@ -416,7 +553,7 @@ extension ConfigurationTelemetry { sessionReplaySampleRate: other.sessionReplaySampleRate ?? sessionReplaySampleRate, sessionSampleRate: other.sessionSampleRate ?? sessionSampleRate, silentMultipleInit: other.silentMultipleInit ?? silentMultipleInit, - startSessionReplayRecordingManually: other.startSessionReplayRecordingManually ?? startSessionReplayRecordingManually, + startRecordingImmediately: other.startRecordingImmediately ?? startRecordingImmediately, telemetryConfigurationSampleRate: other.telemetryConfigurationSampleRate ?? telemetryConfigurationSampleRate, telemetrySampleRate: other.telemetrySampleRate ?? telemetrySampleRate, tracerAPI: other.tracerAPI ?? tracerAPI, diff --git a/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift index 80ac415d77..c184ec3d31 100644 --- a/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift +++ b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift @@ -25,5 +25,30 @@ public protocol FeatureRequestBuilder { /// - context: The current core context. /// - events: The events data to be uploaded. /// - Returns: The URL request. - func request(for events: [Event], with context: DatadogContext) throws -> URLRequest + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest +} + +/// Represents the context in which the request is being executed. +public struct ExecutionContext { + /// HTTP status code of the previous response. + public let previousResponseCode: Int? + + /// The current attempt number. + public let attempt: UInt + + /// Initializes the execution context. + /// - Parameters: + /// - previousResponseCode: Previous HTTP status code, if available. + /// - attempt: The current attempt number. + public init( + previousResponseCode: Int?, + attempt: UInt + ) { + self.previousResponseCode = previousResponseCode + self.attempt = attempt + } } diff --git a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift index 38f6625fa5..8104417bc0 100644 --- a/DatadogInternal/Sources/Upload/URLRequestBuilder.swift +++ b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift @@ -23,6 +23,7 @@ public struct URLRequestBuilder { public static let ddEVPOriginHeaderField = "DD-EVP-ORIGIN" public static let ddEVPOriginVersionHeaderField = "DD-EVP-ORIGIN-VERSION" public static let ddRequestIDHeaderField = "DD-REQUEST-ID" + public static let ddIdempotencyKeyHeaderField = "DD-IDEMPOTENCY-KEY" public enum ContentType { case applicationJSON @@ -91,6 +92,13 @@ public struct URLRequestBuilder { public static func ddRequestIDHeader() -> HTTPHeader { return HTTPHeader(field: ddRequestIDHeaderField, value: { UUID().uuidString }) } + + /// An optional Datadog header for ensuring idempotent requests. + /// - Parameter key: The idempotency key. + /// - Returns: Header with the idempotency key. + public static func ddIdempotencyKeyHeader(key: String) -> HTTPHeader { + return HTTPHeader(field: ddIdempotencyKeyHeaderField, value: { key }) + } } /// Upload `URL`. private let url: URL 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/Sources/Utils/DateFormatting.swift b/DatadogInternal/Sources/Utils/DateFormatting.swift index 7dc4f94cf5..ec32ed44d5 100644 --- a/DatadogInternal/Sources/Utils/DateFormatting.swift +++ b/DatadogInternal/Sources/Utils/DateFormatting.swift @@ -6,31 +6,20 @@ import Foundation -public protocol DateFormatterType { +public protocol DateFormatterType: Sendable { func string(from date: Date) -> String func date(from string: String) -> Date? } -extension ISO8601DateFormatter: DateFormatterType {} -extension DateFormatter: DateFormatterType {} +extension ISO8601DateFormatter: DateFormatterType, @unchecked Sendable {} +extension DateFormatter: DateFormatterType, @unchecked Sendable {} /// Date formatter producing `ISO8601` string representation of a given date. /// Should be used to encode dates in messages send to the server. public let iso8601DateFormatter: DateFormatterType = { - // As there is a known crash in iOS 11.0 and 11.1 when using `.withFractionalSeconds` option in `ISO8601DateFormatter`, - // we use different `DateFormatterType` implementation depending on the OS version. The problem was fixed by Apple in iOS 11.2. - if #available(iOS 11.2, *) { - let formatter = ISO8601DateFormatter() - formatter.formatOptions.insert(.withFractionalSeconds) - return formatter - } else { - let iso8601Formatter = DateFormatter() - iso8601Formatter.locale = Locale(identifier: "en_US_POSIX") - iso8601Formatter.timeZone = TimeZone(abbreviation: "UTC")! // swiftlint:disable:this force_unwrapping - iso8601Formatter.calendar = Calendar(identifier: .gregorian) - iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" // ISO8601 format - return iso8601Formatter - } + let formatter = ISO8601DateFormatter() + formatter.formatOptions.insert(.withFractionalSeconds) + return formatter }() /// Date formatter producing string representation of a given date for user-facing features (like console output). diff --git a/DatadogInternal/Sources/Utils/Sampler.swift b/DatadogInternal/Sources/Utils/Sampler.swift index e87a82f981..4e7c2e6e4d 100644 --- a/DatadogInternal/Sources/Utils/Sampler.swift +++ b/DatadogInternal/Sources/Utils/Sampler.swift @@ -6,6 +6,10 @@ import Foundation +/// Alias to represent the sample rate type. +/// The value is between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. +public typealias SampleRate = Float + /// Protocol for determining sampling decisions. public protocol Sampling { /// Determines whether sampling should be performed. @@ -18,9 +22,9 @@ public protocol Sampling { /// Sampler, deciding if events should be sent do Datadog or dropped. public struct Sampler: Sampling { /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. - public let samplingRate: Float + public let samplingRate: SampleRate - public init(samplingRate: Float) { + public init(samplingRate: SampleRate) { self.samplingRate = max(0, min(100, samplingRate)) } @@ -33,15 +37,42 @@ public struct Sampler: Sampling { /// A sampler that determines sampling decisions deterministically (the same each time). internal struct DeterministicSampler: Sampling { + enum Constants { + /// Good number for Knuth hashing (large, prime, fit in 64 bit long) + internal static let samplerHasher: UInt64 = 1_111_111_111_111_111_111 + internal static let maxID: UInt64 = 0xFFFFFFFFFFFFFFFF + } + /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. - let samplingRate: Float + let samplingRate: SampleRate /// Persisted sampling decision. private let shouldSample: Bool - init(shouldSample: Bool, samplingRate: Float) { + init(shouldSample: Bool, samplingRate: SampleRate) { self.samplingRate = samplingRate self.shouldSample = shouldSample } + init(baseId: UInt64, samplingRate: SampleRate) { + // We use overflow multiplication to create a "randomized" hash based on the input id + let hash = baseId &* Constants.samplerHasher + let threshold = Float(Constants.maxID) * samplingRate.percentageProportion + self.samplingRate = samplingRate + self.shouldSample = Float(hash) < threshold + } + func sample() -> Bool { shouldSample } } + +extension SampleRate { + /// Maximum sampling rate. It means every event is kept. + public static let maxSampleRate: Self = 100.0 + + /// Represents the percentage expressed as a decimal between 0 and 1. For example, 0.25 means 25%. + public var percentageProportion: Self { self / 100.0 } + + /// Composes two sample rates. For example, one SampleRate of 20% composed with another of 15% will return a percentage of 3%. + public func composed(with sampleRate: SampleRate) -> Self { + self.percentageProportion * sampleRate.percentageProportion * 100 + } +} diff --git a/DatadogInternal/Sources/Utils/UIKitExtensions.swift b/DatadogInternal/Sources/Utils/UIKitExtensions.swift index c484916cc2..c9984f3dc6 100644 --- a/DatadogInternal/Sources/Utils/UIKitExtensions.swift +++ b/DatadogInternal/Sources/Utils/UIKitExtensions.swift @@ -6,7 +6,7 @@ import Foundation -#if canImport(UIKit) +#if canImport(UIKit) && !os(watchOS) import UIKit extension DatadogExtension where ExtendedType == UIApplication { diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift b/DatadogInternal/Sources/Utils/WatchKitExtensions.swift similarity index 50% rename from DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift rename to DatadogInternal/Sources/Utils/WatchKitExtensions.swift index 2ba25d2d9a..8b08c2267d 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogPrivateMocks.swift +++ b/DatadogInternal/Sources/Utils/WatchKitExtensions.swift @@ -5,16 +5,15 @@ */ import Foundation -import DatadogCore -class ObjcExceptionHandlerMock: __dd_private_ObjcExceptionHandler { - let error: Error +#if canImport(WatchKit) +import WatchKit - init(throwingError: Error) { - self.error = throwingError - } - - override func rethrowToSwift(tryBlock: @escaping () -> Void) throws { - throw error +extension DatadogExtension where ExtendedType == WKExtension { + public static var shared: WKExtension { + .shared() } } + +extension WKExtension: DatadogExtended { } +#endif diff --git a/DatadogInternal/Tests/Context/DeviceInfoTests.swift b/DatadogInternal/Tests/Context/DeviceInfoTests.swift index 8c5e2c1af5..a2b7f297ee 100644 --- a/DatadogInternal/Tests/Context/DeviceInfoTests.swift +++ b/DatadogInternal/Tests/Context/DeviceInfoTests.swift @@ -32,4 +32,46 @@ class DeviceInfoTests: XCTestCase { XCTAssertEqual(info.osVersion, randomOSVersion) XCTAssertNotNil(info.osBuildNumber) } + + func testDeviceType() { + // Given + let iPhone = UIDeviceMock(model: "iPhone14,5", systemName: "iOS") + let iPod = UIDeviceMock(model: "iPod7,1", systemName: "iOS") + let iPad = UIDeviceMock(model: "iPad12,1", systemName: "iPadOS") + let appleTV1 = UIDeviceMock(model: "J305AP", systemName: "tvOS") + let appleTV2 = UIDeviceMock(model: "AppleTV14,1 Simulator", systemName: "tvOS") + let other = UIDeviceMock(model: "RealityDevice14,1", systemName: "visionOS") + + // When / Then + func when(device: UIDeviceMock) -> DeviceInfo { + return DeviceInfo(processInfo: ProcessInfoMock(), device: device) + } + + XCTAssertEqual(when(device: iPhone).type, .iPhone) + XCTAssertEqual(when(device: iPod).type, .iPod) + XCTAssertEqual(when(device: iPad).type, .iPad) + XCTAssertEqual(when(device: appleTV1).type, .appleTV) + XCTAssertEqual(when(device: appleTV2).type, .appleTV) + XCTAssertEqual(when(device: other).type, .other(modelName: "RealityDevice14,1 Simulator", osName: "visionOS")) + } + + func testOSVersionMajor() { + // When + func when(systemVersion: String) -> DeviceInfo { + return DeviceInfo( + processInfo: ProcessInfoMock(), + device: UIDeviceMock(systemVersion: systemVersion) + ) + } + + // Then + XCTAssertEqual(when(systemVersion: "15.4.1").osVersion, "15.4.1") + XCTAssertEqual(when(systemVersion: "15.4.1").osVersionMajor, "15") + + XCTAssertEqual(when(systemVersion: "17.0").osVersion, "17.0") + XCTAssertEqual(when(systemVersion: "17.0").osVersionMajor, "17") + + XCTAssertEqual(when(systemVersion: "18").osVersion, "18") + XCTAssertEqual(when(systemVersion: "18").osVersionMajor, "18") + } } diff --git a/DatadogInternal/Tests/CoreRegistryTest.swift b/DatadogInternal/Tests/CoreRegistryTest.swift index fc57718202..aed89048bc 100644 --- a/DatadogInternal/Tests/CoreRegistryTest.swift +++ b/DatadogInternal/Tests/CoreRegistryTest.swift @@ -5,7 +5,7 @@ */ import XCTest -import TestUtilities +@testable import TestUtilities @testable import DatadogInternal @@ -46,4 +46,44 @@ class CoreRegistryTest: XCTestCase { CoreRegistry.unregisterDefault() CoreRegistry.unregisterInstance(named: "test") } + + func testIsFeatureEnabled_whenFeatureIsRegistered_itReturnsTrue() { + // Given + let core = FeatureRegistrationCoreMock() + let feature = MockFeature() + + // Register the mock feature in the core + try? core.register(feature: feature) + + // Register the core in the CoreRegistry + CoreRegistry.register(default: core) + + // When + let isEnabled = CoreRegistry.isFeatureEnabled(feature: MockFeature.self) + + // Then + XCTAssertTrue(isEnabled) + + // Cleanup + CoreRegistry.unregisterDefault() + } + + func testIsFeatureEnabled_whenFeatureIsNotRegistered_itReturnsFalse() { + // Given + let core = FeatureRegistrationCoreMock() + + // No feature registered + + // Register the core in the CoreRegistry + CoreRegistry.register(default: core) + + // When + let isEnabled = CoreRegistry.isFeatureEnabled(feature: MockFeature.self) + + // Then + XCTAssertFalse(isEnabled) + + // Cleanup + CoreRegistry.unregisterDefault() + } } diff --git a/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift b/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift new file mode 100644 index 0000000000..a7dc318b49 --- /dev/null +++ b/DatadogInternal/Tests/Extensions/Data+CryptoTests.swift @@ -0,0 +1,28 @@ +/* + * 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 + +final class DataCryptoTests: XCTestCase { + func testSha1() throws { + let str1 = "The quick brown fox jumps over the lazy dog" + let data1 = str1.data(using: .utf8)! + let sha1 = data1.sha1() + XCTAssertEqual(sha1, "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") + + let str2 = "The quick brown fox jumps over the lazy cog" + let data2 = str2.data(using: .utf8)! + let sha2 = data2.sha1() + XCTAssertEqual(sha2, "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3") + } + + func testSha1_emptyString() throws { + let str = "" + let data = str.data(using: .utf8)! + let sha = data.sha1() + XCTAssertEqual(sha, "da39a3ee5e6b4b0d3255bfef95601890afd80709") + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionInterceptorTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionInterceptorTests.swift new file mode 100644 index 0000000000..17976139f0 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionInterceptorTests.swift @@ -0,0 +1,70 @@ +/* + * 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 URLSessionInterceptorTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + private var core: SingleFeatureCoreMock! + private var handler: URLSessionHandlerMock! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + + core = SingleFeatureCoreMock() + handler = URLSessionHandlerMock() + try core.register(urlSessionHandler: handler) + } + + override func tearDown() { + core = nil + super.tearDown() + } + + func testTraceInterception() throws { + // Given + let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) + let trace: TraceContext = .mockWith(isKept: true) + let writer: TracePropagationHeadersWriter = oneOf([ + { HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) }, + { B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) }, + { W3CHTTPHeadersWriter(samplingStrategy: .headBased) } + ]) + + let url: URL = .mockAny() + handler.firstPartyHosts = .init( + hostsWithTracingHeaderTypes: [url.host!: [.datadog]] + ) + + writer.write(traceContext: trace) + handler.modifiedRequest = .mockWith(url: url, headers: writer.traceHeaderFields) + handler.injectedTraceContext = trace + + // When + var interceptor = try XCTUnwrap(URLSessionInterceptor.shared(in: core)) + let request = interceptor.intercept(request: .mockWith(url: url)) + + let task: URLSessionTask = .mockWith(request: request) + interceptor = try XCTUnwrap(URLSessionInterceptor.shared(in: core)) + interceptor.intercept(task: task) + + interceptor = try XCTUnwrap(URLSessionInterceptor.shared(in: core)) + interceptor.task(task, didCompleteWithError: nil) + + handler.onInterceptionDidStart = { interception in + // Then + XCTAssertEqual(interception.trace, trace) + } + + feature.flush() + + XCTAssert(URLSessionInterceptor.contextsByTraceID.isEmpty) + } +} diff --git a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift index 3b804681c3..23a1271f59 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift @@ -20,8 +20,11 @@ extension ConfigurationTelemetry { batchSize: .mockRandom(), batchUploadFrequency: .mockRandom(), dartVersion: .mockRandom(), - defaultPrivacyLevel: .mockRandom(), forwardErrorsToLogs: .mockRandom(), + defaultPrivacyLevel: .mockRandom(), + textAndInputPrivacyLevel: .mockRandom(), + imagePrivacyLevel: .mockRandom(), + touchPrivacyLevel: .mockRandom(), initializationType: .mockRandom(), mobileVitalsUpdatePeriod: .mockRandom(), reactNativeVersion: .mockRandom(), @@ -29,7 +32,7 @@ extension ConfigurationTelemetry { sessionReplaySampleRate: .mockRandom(), sessionSampleRate: .mockRandom(), silentMultipleInit: .mockRandom(), - startSessionReplayRecordingManually: .mockRandom(), + startRecordingImmediately: .mockRandom(), telemetryConfigurationSampleRate: .mockRandom(), telemetrySampleRate: .mockRandom(), tracerAPI: .mockRandom(), diff --git a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift index f61db878ac..4b3a45a20a 100644 --- a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift +++ b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift @@ -109,32 +109,56 @@ class TelemetryTests: XCTestCase { func testSendingConfigurationTelemetry() throws { // When - telemetry.configuration(batchSize: 123, batchUploadFrequency: 456) // only some values + telemetry.configuration(backgroundTasksEnabled: true, batchSize: 123, batchUploadFrequency: 456) // only some values // Then let configuration = try XCTUnwrap(telemetry.messages.firstConfiguration()) XCTAssertEqual(configuration.batchSize, 123) XCTAssertEqual(configuration.batchUploadFrequency, 456) + XCTAssertEqual(configuration.backgroundTasksEnabled, true) } // MARK: - Metric Telemetry func testSendingMetricTelemetry() throws { // When - telemetry.metric(name: "metric name", attributes: ["attribute": "value"]) + telemetry.metric(name: "metric name", attributes: ["attribute": "value"], sampleRate: 4.21) // Then let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) XCTAssertEqual(metric.name, "metric name") XCTAssertEqual(metric.attributes as? [String: String], ["attribute": "value"]) + XCTAssertEqual(metric.sampleRate, 4.21) } - func testStartingMethodCalledMetricTrace_whenSampled() throws { - XCTAssertNotNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 100)) + func testMetricTelemetryDefaultSampleRate() throws { + // When + telemetry.metric(name: "metric name", attributes: [:]) + + // Then + let metric = try XCTUnwrap(telemetry.messages.compactMap({ $0.asMetric }).first) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) + } + + func testHeadSampleRateInMethodCalledMetric() throws { + XCTAssertNotNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100)) + XCTAssertNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 0)) } - func testStartingMethodCalledMetricTrace_whenNotSampled() throws { - XCTAssertNil(telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), samplingRate: 0)) + func testDefaultTailSampleRateInMethodCalledMetric() throws { + let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) + telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny()) + + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) + } + + func testTailSampleRateInMethodCalledMetric() throws { + let metricTrace = telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) + telemetry.stopMethodCalled(metricTrace, isSuccessful: .mockAny(), tailSampleRate: 42.5) + + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) + XCTAssertEqual(metric.sampleRate, 42.5) } func testTrackingMethodCallMetricTelemetry() throws { @@ -143,19 +167,21 @@ class TelemetryTests: XCTestCase { let isSuccessful: Bool = .random() // When - let metricTrace = telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, samplingRate: 100) + let metricTrace = telemetry.startMethodCalled(operationName: operationName, callerClass: callerClass, headSampleRate: 100) Thread.sleep(forTimeInterval: 0.05) telemetry.stopMethodCalled(metricTrace, isSuccessful: isSuccessful) // Then let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: MethodCalledMetric.name)) XCTAssertEqual(metric.attributes[SDKMetricFields.typeKey] as? String, MethodCalledMetric.typeValue) + XCTAssertEqual(metric.attributes[SDKMetricFields.headSampleRate] as? SampleRate, 100) XCTAssertEqual(metric.attributes[MethodCalledMetric.operationName] as? String, operationName) XCTAssertEqual(metric.attributes[MethodCalledMetric.callerClass] as? String, callerClass) XCTAssertEqual(metric.attributes[MethodCalledMetric.isSuccessful] as? Bool, isSuccessful) let executionTime = try XCTUnwrap(metric.attributes[MethodCalledMetric.executionTime] as? Int64) XCTAssertGreaterThan(executionTime, 0) XCTAssertLessThan(executionTime, TimeInterval(1).toInt64Nanoseconds) + XCTAssertEqual(metric.sampleRate, MetricTelemetry.defaultSampleRate) } // MARK: - Integration with Core @@ -173,10 +199,10 @@ class TelemetryTests: XCTestCase { core.telemetry.configuration(batchSize: 123) XCTAssertEqual(receiver.messages.lastTelemetry?.asConfiguration?.batchSize, 123) - core.telemetry.metric(name: "metric name", attributes: [:]) + core.telemetry.metric(name: "metric name", attributes: [:], sampleRate: 15) XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, "metric name") - let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny()) + let metricTrace = core.telemetry.startMethodCalled(operationName: .mockAny(), callerClass: .mockAny(), headSampleRate: 100) core.telemetry.stopMethodCalled(metricTrace) XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, MethodCalledMetric.name) } diff --git a/DatadogInternal/Tests/Utils/DeterministicSamplerTests.swift b/DatadogInternal/Tests/Utils/DeterministicSamplerTests.swift new file mode 100644 index 0000000000..a735fbbeb2 --- /dev/null +++ b/DatadogInternal/Tests/Utils/DeterministicSamplerTests.swift @@ -0,0 +1,59 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogInternal + + class DeterministicSamplerTests: XCTestCase { + private let measurements = 0..<128 + + func testWhenInitWithNotSampled_itAlwaysReturnsNotSampled() { + // Given + let fakeSampleRate = Float.random(in: 0.0..<100.0) + let sampler = DeterministicSampler(shouldSample: false, samplingRate: fakeSampleRate) + + // When + var notSampledCount = 0 + measurements.forEach { _ in + notSampledCount += sampler.sample() ? 0 : 1 + } + + // Then + XCTAssertEqual(notSampledCount, measurements.count) + } + + func testWhenInitWithIsSampled_itAlwaysReturnsIsSampled() { + // Given + let fakeSampleRate = Float.random(in: 0.0..<100.0) + let sampler = DeterministicSampler(shouldSample: true, samplingRate: fakeSampleRate) + + // When + var isSampledCount = 0 + measurements.forEach { _ in + isSampledCount += sampler.sample() ? 1 : 0 + } + + // Then + XCTAssertEqual(isSampledCount, measurements.count) + } + + func testWithHardcodedTraceId_itReturnsExpectedDecision() { + XCTAssertEqual(DeterministicSampler(baseId: 4_815_162_342, samplingRate: 55.9).sample(), false) + XCTAssertEqual(DeterministicSampler(baseId: 4_815_162_342, samplingRate: 56.0).sample(), true) + + XCTAssertEqual(DeterministicSampler(baseId: 1_415_926_535_897_932_384, samplingRate: 90.5).sample(), false) + XCTAssertEqual(DeterministicSampler(baseId: 1_415_926_535_897_932_384, samplingRate: 90.6).sample(), true) + + XCTAssertEqual(DeterministicSampler(baseId: 718_281_828_459_045_235, samplingRate: 7.4).sample(), false) + XCTAssertEqual(DeterministicSampler(baseId: 718_281_828_459_045_235, samplingRate: 7.5).sample(), true) + + XCTAssertEqual(DeterministicSampler(baseId: 41_421_356_237_309_504, samplingRate: 32.1).sample(), false) + XCTAssertEqual(DeterministicSampler(baseId: 41_421_356_237_309_504, samplingRate: 32.2).sample(), true) + + XCTAssertEqual(DeterministicSampler(baseId: 6_180_339_887_498_948_482, samplingRate: 68.2).sample(), false) + XCTAssertEqual(DeterministicSampler(baseId: 6_180_339_887_498_948_482, samplingRate: 68.3).sample(), true) + } + } 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/DatadogInternal/Tests/Utils/SampleRateTests.swift b/DatadogInternal/Tests/Utils/SampleRateTests.swift new file mode 100644 index 0000000000..41bd451d2f --- /dev/null +++ b/DatadogInternal/Tests/Utils/SampleRateTests.swift @@ -0,0 +1,82 @@ +/* + * 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 DatadogInternal + +final class SampleRateTests: XCTestCase { + func testPercentageProportion() { + // Given + let zeroSampleRate: SampleRate = 0.0 + let sampleRate: SampleRate = 50.0 + let fullSampleRate: SampleRate = 100.0 + + // Then + XCTAssertEqual(zeroSampleRate.percentageProportion, 0.0) + XCTAssertEqual(sampleRate.percentageProportion, 0.5) + XCTAssertEqual(fullSampleRate.percentageProportion, 1.0) + } + + func testComposedSampleRate() { + // Given + let sampleRate1: SampleRate = 20.0 + let sampleRate2: SampleRate = 15.0 + + // When + let composedRate = sampleRate1.composed(with: sampleRate2) + let composedRateInverted = sampleRate2.composed(with: sampleRate1) + let composedRateWithFullSampleRate = sampleRate1.composed(with: .maxSampleRate) + + // Then + XCTAssertEqual(composedRate, 3.0) + XCTAssertEqual(composedRateInverted, 3.0) + XCTAssertEqual(composedRateWithFullSampleRate, sampleRate1) + } + + func testComposedSampleRateWithZeroSampleRate() { + // Given + let sampleRate1: SampleRate = 0.0 + let sampleRate2: SampleRate = 15.0 + + // When + let composedRate = sampleRate1.composed(with: sampleRate2) + let composedRateInverted = sampleRate2.composed(with: sampleRate1) + + // Then + XCTAssertEqual(composedRate, 0.0) + XCTAssertEqual(composedRateInverted, 0.0) + } + + func testComposedSampleRateWithFullSampleRate() { + // Given + let sampleRate1: SampleRate = .maxSampleRate + let sampleRate2: SampleRate = .maxSampleRate + + // When + let composedRate = sampleRate1.composed(with: sampleRate2) + let composedRateInverted = sampleRate2.composed(with: sampleRate1) + + // Then + XCTAssertEqual(composedRate, .maxSampleRate) + XCTAssertEqual(composedRateInverted, .maxSampleRate) + } + + func testComposedSampleWithMultipleSampleRates() { + // Given + let sampleRate1: SampleRate = .maxSampleRate + let sampleRate2: SampleRate = 50.0 + let sampleRate3: SampleRate = 20.0 + let sampleRate4: SampleRate = 15.0 + + // When + let composedRateWith3Layers = sampleRate1.composed(with: sampleRate2).composed(with: sampleRate3) + let composedRateWith4Layers = sampleRate1.composed(with: sampleRate2).composed(with: sampleRate3).composed(with: sampleRate4) + + // Then + XCTAssertEqual(composedRateWith3Layers, 10.0) + XCTAssertEqual(composedRateWith4Layers, 1.5) + } +} diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec index 110ed51c2a..7eed5cf060 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.22.0" s.summary = "Datadog Logs Module." s.homepage = "https://www.datadoghq.com" @@ -17,6 +17,7 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.ios.deployment_target = '12.0' s.tvos.deployment_target = '12.0' + s.watchos.deployment_target = '7.0' s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } diff --git a/DatadogLogs/Sources/ConsoleLogger.swift b/DatadogLogs/Sources/ConsoleLogger.swift index d2a793cea0..4e42d1b0ae 100644 --- a/DatadogLogs/Sources/ConsoleLogger.swift +++ b/DatadogLogs/Sources/ConsoleLogger.swift @@ -23,12 +23,12 @@ internal final class ConsoleLogger: LoggerProtocol { /// The prefix to use when rendering log. private let prefix: String /// The function used to render log. - private let printFunction: (String, CoreLoggerLevel) -> Void + private let printFunction: @Sendable (String, CoreLoggerLevel) -> Void init( configuration: Configuration, dateProvider: DateProvider, - printFunction: @escaping (String, CoreLoggerLevel) -> Void + printFunction: @escaping @Sendable (String, CoreLoggerLevel) -> Void ) { self.dateProvider = dateProvider self.timeFormatter = presentationDateFormatter(withTimeZone: configuration.timeZone) diff --git a/DatadogLogs/Sources/Feature/LogsFeature.swift b/DatadogLogs/Sources/Feature/LogsFeature.swift index ffd1fa1794..fa8159d2a4 100644 --- a/DatadogLogs/Sources/Feature/LogsFeature.swift +++ b/DatadogLogs/Sources/Feature/LogsFeature.swift @@ -18,8 +18,8 @@ internal struct LogsFeature: DatadogRemoteFeature { let backtraceReporter: BacktraceReporting? - @ReadWriteLock - private var attributes: [String: Encodable] = [:] + /// Global attributes attached to every log event. + let attributes: SynchronizedAttributes /// Time provider. let dateProvider: DateProvider @@ -59,17 +59,6 @@ internal struct LogsFeature: DatadogRemoteFeature { self.messageReceiver = messageReceiver self.dateProvider = dateProvider self.backtraceReporter = backtraceReporter - } - - internal func addAttribute(forKey key: AttributeKey, value: AttributeValue) { - _attributes.mutate { $0[key] = value } - } - - internal func removeAttribute(forKey key: AttributeKey) { - _attributes.mutate { $0.removeValue(forKey: key) } - } - - internal func getAttributes() -> [String: Encodable] { - return attributes + self.attributes = SynchronizedAttributes(attributes: [:]) } } diff --git a/DatadogLogs/Sources/Feature/MessageReceivers.swift b/DatadogLogs/Sources/Feature/MessageReceivers.swift index a74bc715db..78b0d6e556 100644 --- a/DatadogLogs/Sources/Feature/MessageReceivers.swift +++ b/DatadogLogs/Sources/Feature/MessageReceivers.swift @@ -213,7 +213,11 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { let user = crashContext.userInfo let deviceInfo = crashContext.device - let userAttributes = crashContext.lastLogAttributes?.attributes + + // Merge logs attributes with crash report attributes + let lastLogAttributes = crashContext.lastLogAttributes?.attributes ?? [:] + let additionalAttributes: [String: Encodable] = report.additionalAttributes.dd.decode() ?? [:] + let userAttributes = lastLogAttributes.merging(additionalAttributes) { _, new in new } // crash reporting is considering the user consent from previous session, if an event reached // the message bus it means that consent was granted and we can safely bypass current consent. @@ -259,7 +263,7 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { networkConnectionInfo: crashContext.networkConnectionInfo, mobileCarrierInfo: crashContext.carrierInfo, attributes: .init( - userAttributes: userAttributes ?? [:], + userAttributes: userAttributes, internalAttributes: errorAttributes ), tags: nil diff --git a/DatadogLogs/Sources/Feature/RequestBuilder.swift b/DatadogLogs/Sources/Feature/RequestBuilder.swift index 892f57683a..1444c19425 100644 --- a/DatadogLogs/Sources/Feature/RequestBuilder.swift +++ b/DatadogLogs/Sources/Feature/RequestBuilder.swift @@ -27,11 +27,15 @@ internal struct RequestBuilder: FeatureRequestBuilder { self.telemetry = telemetry } - func request(for events: [Event], with context: DatadogContext) -> URLRequest { + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) -> URLRequest { let builder = URLRequestBuilder( url: url(with: context), queryItems: [ - .ddsource(source: context.source) + .ddsource(source: context.source), ], headers: [ .contentTypeHeader(contentType: .applicationJSON), diff --git a/DatadogLogs/Sources/Log/SynchronizedAttributes.swift b/DatadogLogs/Sources/Log/SynchronizedAttributes.swift new file mode 100644 index 0000000000..d0270ed7cd --- /dev/null +++ b/DatadogLogs/Sources/Log/SynchronizedAttributes.swift @@ -0,0 +1,47 @@ +/* + * 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 DatadogInternal + +/// A thread-safe container for managing attributes in a key-value format. +/// This class allows concurrent access and modification of attributes, ensuring data consistency +/// through the use of a `ReadWriteLock`. It is designed to be used in scenarios where attributes +/// need to be safely managed across multiple threads or tasks. +internal final class SynchronizedAttributes: Sendable { + /// The underlying dictionary of attributes, wrapped in a `ReadWriteLock` to ensure thread safety. + private let attributes: ReadWriteLock<[String: Encodable]> + + /// Initializes a new instance of `SynchronizedAttributes` with the provided dictionary. + /// + /// - Parameter attributes: A dictionary of initial attributes. + init(attributes: [String: Encodable]) { + self.attributes = .init(wrappedValue: attributes) + } + + /// Adds or updates an attribute in the container. + /// + /// - Parameters: + /// - key: The key associated with the attribute. + /// - value: The value to associate with the key. + func addAttribute(key: AttributeKey, value: AttributeValue) { + attributes.mutate { $0[key] = value } + } + + /// Removes an attribute from the container. + /// + /// - Parameter key: The key of the attribute to remove. + func removeAttribute(forKey key: AttributeKey) { + attributes.mutate { $0.removeValue(forKey: key) } + } + + /// Retrieves the current dictionary of attributes. + /// + /// - Returns: A dictionary containing all the attributes. + func getAttributes() -> [String: Encodable] { + return attributes.wrappedValue + } +} diff --git a/DatadogLogs/Sources/Log/SynchronizedTags.swift b/DatadogLogs/Sources/Log/SynchronizedTags.swift new file mode 100644 index 0000000000..273b3a4e35 --- /dev/null +++ b/DatadogLogs/Sources/Log/SynchronizedTags.swift @@ -0,0 +1,52 @@ +/* + * 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 DatadogInternal + +/// A thread-safe container for managing tags in a set. +/// This class allows concurrent access and modification of tags, ensuring data consistency +/// through the use of a `ReadWriteLock`. It is designed to be used in scenarios where tags +/// need to be safely managed across multiple threads or tasks. +internal final class SynchronizedTags: Sendable { + /// The underlying set of tags, wrapped in a `ReadWriteLock` to ensure thread safety. + private let tags: ReadWriteLock> + + /// Initializes a new instance of `SynchronizedTags` with the provided set. + /// + /// - Parameter tags: A set of initial tags. + init(tags: Set) { + self.tags = .init(wrappedValue: tags) + } + + /// Adds a tag to the set. + /// + /// - Parameter tag: The tag to add. + func addTag(_ tag: String) { + tags.mutate { $0.insert(tag) } + } + + /// Removes a tag from the set. + /// + /// - Parameter tag: The tag to remove. + func removeTag(_ tag: String) { + tags.mutate { $0.remove(tag) } + } + + /// Removes tags from the set based on a predicate. + /// + /// - Parameter shouldRemove: A closure that takes a tag and returns `true` if the tag should be removed. + func removeTags(where shouldRemove: (String) -> Bool) { + tags.mutate { $0 = $0.filter { !shouldRemove($0) } } + } + + /// Retrieves the current set of tags. + /// + /// - Returns: A set containing all the tags. + func getTags() -> Set { + return tags.wrappedValue + } +} diff --git a/DatadogLogs/Sources/Logger.swift b/DatadogLogs/Sources/Logger.swift index b96b731d7b..c50da3dc01 100644 --- a/DatadogLogs/Sources/Logger.swift +++ b/DatadogLogs/Sources/Logger.swift @@ -92,7 +92,7 @@ public struct Logger { networkInfoEnabled: Bool = false, bundleWithRumEnabled: Bool = true, bundleWithTraceEnabled: Bool = true, - remoteSampleRate: Float = 100, + remoteSampleRate: Float = .maxSampleRate, remoteLogThreshold: LogLevel = .debug, consoleLogFormat: ConsoleLogFormat? = nil ) { @@ -136,13 +136,13 @@ public struct Logger { private static func createOrThrow(with configuration: Configuration, in core: DatadogCoreProtocol) throws -> LoggerProtocol { if core is NOPDatadogCore { throw ProgrammerError( - description: "`Datadog.initialize()` must be called prior to `Logger.builder.build()`." + description: "`Datadog.initialize()` must be called prior to `Logger.create()`." ) } guard let feature = core.get(feature: LogsFeature.self) else { throw ProgrammerError( - description: "`Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + description: "`Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." ) } @@ -154,7 +154,8 @@ public struct Logger { } return RemoteLogger( - core: core, + featureScope: core.scope(for: LogsFeature.self), + globalAttributes: feature.attributes, configuration: RemoteLogger.Configuration( service: configuration.service, name: configuration.name, @@ -165,7 +166,8 @@ public struct Logger { ), dateProvider: feature.dateProvider, rumContextIntegration: configuration.bundleWithRumEnabled, - activeSpanIntegration: configuration.bundleWithTraceEnabled + activeSpanIntegration: configuration.bundleWithTraceEnabled, + backtraceReporter: feature.backtraceReporter ) }() diff --git a/DatadogLogs/Sources/LoggerProtocol.swift b/DatadogLogs/Sources/LoggerProtocol.swift index f0c7c55cb2..07c28adda0 100644 --- a/DatadogLogs/Sources/LoggerProtocol.swift +++ b/DatadogLogs/Sources/LoggerProtocol.swift @@ -39,7 +39,7 @@ extension CoreLoggerLevel { /// /// // logger reference /// var logger = Logger.create() -public protocol LoggerProtocol { +public protocol LoggerProtocol: Sendable { /// General purpose logging method. /// Sends a log with certain `level`, `message`, `error` and `attributes`. /// diff --git a/DatadogLogs/Sources/Logs.swift b/DatadogLogs/Sources/Logs.swift index 62ac807df6..19534e6ab4 100644 --- a/DatadogLogs/Sources/Logs.swift +++ b/DatadogLogs/Sources/Logs.swift @@ -64,7 +64,8 @@ public enum Logs { logEventMapper: logEventMapper, dateProvider: configuration.dateProvider, customIntakeURL: configuration.customEndpoint, - telemetry: core.telemetry + telemetry: core.telemetry, + backtraceReporter: core.backtraceReporter ) do { @@ -84,7 +85,7 @@ public enum Logs { guard let feature = core.get(feature: LogsFeature.self) else { return } - feature.addAttribute(forKey: key, value: value) + feature.attributes.addAttribute(key: key, value: value) sendAttributesChanged(for: feature, in: core) } @@ -98,7 +99,7 @@ public enum Logs { guard let feature = core.get(feature: LogsFeature.self) else { return } - feature.removeAttribute(forKey: key) + feature.attributes.removeAttribute(forKey: key) sendAttributesChanged(for: feature, in: core) } @@ -106,7 +107,7 @@ public enum Logs { core.send( message: .baggage( key: GlobalLogAttributes.key, - value: GlobalLogAttributes(attributes: feature.getAttributes()) + value: GlobalLogAttributes(attributes: feature.attributes.getAttributes()) ) ) } diff --git a/DatadogLogs/Sources/RemoteLogger.swift b/DatadogLogs/Sources/RemoteLogger.swift index 1506defdf7..308f9a1077 100644 --- a/DatadogLogs/Sources/RemoteLogger.swift +++ b/DatadogLogs/Sources/RemoteLogger.swift @@ -8,8 +8,8 @@ import Foundation import DatadogInternal /// `Logger` sending logs to Datadog. -internal final class RemoteLogger: LoggerProtocol { - struct Configuration { +internal final class RemoteLogger: LoggerProtocol, Sendable { + struct Configuration: @unchecked Sendable { /// The `service` value for logs. /// See: [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). let service: String? @@ -25,10 +25,10 @@ internal final class RemoteLogger: LoggerProtocol { let sampler: Sampler } - /// `DatadogCore` instance managing this logger. - internal weak var core: DatadogCoreProtocol? + /// Logs feature scope. + let featureScope: FeatureScope /// Configuration specific to this logger. - internal let configuration: Configuration + let configuration: Configuration /// Date provider for logs. private let dateProvider: DateProvider /// Integration with RUM. It is used to correlate Logs with RUM events by injecting RUM context to `LogEvent`. @@ -37,53 +37,61 @@ internal final class RemoteLogger: LoggerProtocol { /// Integration with Tracing. It is used to correlate Logs with Spans by injecting `Span` context to `LogEvent`. /// Can be `false` if the integration is disabled for this logger. internal let activeSpanIntegration: Bool + /// Global attributes shared with all logger instances. + private let globalAttributes: SynchronizedAttributes /// Logger-specific attributes. - @ReadWriteLock - private var attributes: [String: Encodable] = [:] + private let loggerAttributes: SynchronizedAttributes /// Logger-specific tags. - @ReadWriteLock - private var tags: Set = [] + private let loggerTags: SynchronizedTags + /// Backtrace reporter for attaching binary images to cross-platform errors. + private let backtraceReporter: BacktraceReporting? init( - core: DatadogCoreProtocol, + featureScope: FeatureScope, + globalAttributes: SynchronizedAttributes, configuration: Configuration, dateProvider: DateProvider, rumContextIntegration: Bool, - activeSpanIntegration: Bool + activeSpanIntegration: Bool, + backtraceReporter: BacktraceReporting? ) { - self.core = core + self.featureScope = featureScope + self.globalAttributes = globalAttributes + self.loggerAttributes = SynchronizedAttributes(attributes: [:]) + self.loggerTags = SynchronizedTags(tags: []) self.configuration = configuration self.dateProvider = dateProvider self.rumContextIntegration = rumContextIntegration self.activeSpanIntegration = activeSpanIntegration + self.backtraceReporter = backtraceReporter } // MARK: - Attributes func addAttribute(forKey key: AttributeKey, value: AttributeValue) { - _attributes.mutate { $0[key] = value } + loggerAttributes.addAttribute(key: key, value: value) } func removeAttribute(forKey key: AttributeKey) { - _attributes.mutate { $0.removeValue(forKey: key) } + loggerAttributes.removeAttribute(forKey: key) } // MARK: - Tags func addTag(withKey key: String, value: String) { - _tags.mutate { $0.insert("\(key):\(value)") } + loggerTags.addTag("\(key):\(value)") } func removeTag(withKey key: String) { - _tags.mutate { $0 = $0.filter { !$0.hasPrefix("\(key):") } } + loggerTags.removeTags(where: { $0.hasPrefix("\(key):") }) } func add(tag: String) { - _tags.mutate { $0.insert(tag) } + loggerTags.addTag(tag) } func remove(tag: String) { - _tags.mutate { $0.remove(tag) } + loggerTags.removeTag(tag) } // MARK: - Logging @@ -100,32 +108,32 @@ internal final class RemoteLogger: LoggerProtocol { return } - let logsFeature = self.core?.get(feature: LogsFeature.self) - - let globalAttributes = logsFeature?.getAttributes() - // on user thread: let date = dateProvider.now let threadName = Thread.current.dd.name // capture current tags and attributes before opening the write event context - let tags = self.tags + let tags = loggerTags.getTags() + let globalAttributes = globalAttributes.getAttributes() + let loggerAttributes = loggerAttributes.getAttributes() var logAttributes = attributes - let isCrash = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorLogIsCrash) as? Bool ?? false - let errorFingerprint = logAttributes?.removeValue(forKey: Logs.Attributes.errorFingerprint) as? String - let addBinaryImages = logAttributes?.removeValue(forKey: CrossPlatformAttributes.includeBinaryImages) as? Bool ?? false - let userAttributes = self.attributes - .merging(logAttributes ?? [:]) { $1 } // prefer message attributes - let combinedAttributes: [String: any Encodable] - if let globalAttributes = globalAttributes { - combinedAttributes = globalAttributes.merging(userAttributes) { $1 } - } else { - combinedAttributes = userAttributes - } + + let isCrash = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorLogIsCrash)?.dd.decode() ?? false + let errorFingerprint: String? = logAttributes?.removeValue(forKey: Logs.Attributes.errorFingerprint)?.dd.decode() + let addBinaryImages = logAttributes?.removeValue(forKey: CrossPlatformAttributes.includeBinaryImages)?.dd.decode() ?? false + let userAttributes = loggerAttributes + .merging(logAttributes ?? [:]) { $1 } // prefer `logAttributes`` + + let combinedAttributes: [String: any Encodable] = globalAttributes + .merging(userAttributes) { $1 } // prefer `userAttribute` // SDK context must be requested on the user thread to ensure that it provides values // that are up-to-date for the caller. - core?.scope(for: LogsFeature.self).eventWriteContext { context, writer in + featureScope.eventWriteContext { [weak self] context, writer in + guard let self else { + return + } + var internalAttributes: [String: Encodable] = [:] // When bundle with RUM is enabled, link RUM context (if available): @@ -137,7 +145,7 @@ internal final class RemoteLogger: LoggerProtocol { internalAttributes[LogEvent.Attributes.RUM.viewID] = rum.viewID internalAttributes[LogEvent.Attributes.RUM.actionID] = rum.userActionID } catch { - self.core?.telemetry + self.featureScope.telemetry .error("Fails to decode RUM context from Logs", error: error) } } @@ -149,7 +157,7 @@ internal final class RemoteLogger: LoggerProtocol { internalAttributes[LogEvent.Attributes.Trace.traceID] = trace.traceID?.toString(representation: .hexadecimal) internalAttributes[LogEvent.Attributes.Trace.spanID] = trace.spanID?.toString(representation: .decimal) } catch { - self.core?.telemetry + self.featureScope.telemetry .error("Fails to decode Span context from Logs", error: error) } } @@ -158,7 +166,7 @@ internal final class RemoteLogger: LoggerProtocol { var binaryImages: [BinaryImage]? if addBinaryImages { // TODO: RUM-4072 Replace full backtrace reporter with simpler binary image fetcher - binaryImages = try? logsFeature?.backtraceReporter?.generateBacktrace()?.binaryImages + binaryImages = try? self.backtraceReporter?.generateBacktrace()?.binaryImages } let builder = LogEventBuilder( @@ -189,7 +197,16 @@ internal final class RemoteLogger: LoggerProtocol { return } - self.core?.send( + // Add back in fingerprint and error source type + var busCombinedAttributes = combinedAttributes + if let errorSourcetype = error?.sourceType { + busCombinedAttributes[CrossPlatformAttributes.errorSourceType] = errorSourcetype + } + if let errorFingerprint = errorFingerprint { + busCombinedAttributes[Logs.Attributes.errorFingerprint] = errorFingerprint + } + + self.featureScope.send( message: .baggage( key: ErrorMessage.key, value: ErrorMessage( @@ -197,7 +214,7 @@ internal final class RemoteLogger: LoggerProtocol { message: log.error?.message ?? log.message, type: log.error?.kind, stack: log.error?.stack, - attributes: .init(combinedAttributes), + attributes: .init(busCombinedAttributes), binaryImages: binaryImages ) ) diff --git a/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift b/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift new file mode 100644 index 0000000000..403ff41801 --- /dev/null +++ b/DatadogLogs/Tests/Log/SynchronizedAttributesTests.swift @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogLogs + +final class SynchronizedAttributesTests: XCTestCase { + func testAddAttribute() { + let synchronizedAttributes = SynchronizedAttributes(attributes: [:]) + synchronizedAttributes.addAttribute(key: "key1", value: "value1") + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertEqual(attributes["key1"] as? String, "value1") + XCTAssertEqual(attributes.count, 1) + } + + func testRemoveAttribute() { + let synchronizedAttributes = SynchronizedAttributes(attributes: ["key1": "value1", "key2": "value2"]) + synchronizedAttributes.removeAttribute(forKey: "key1") + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertNil(attributes["key1"]) + XCTAssertEqual(attributes["key2"] as? String, "value2") + XCTAssertEqual(attributes.count, 1) + } + + func testGetAttributes() { + let initialAttributes: [String: Encodable] = ["key1": "value1", "key2": "value2"] + let synchronizedAttributes = SynchronizedAttributes(attributes: initialAttributes) + + let attributes = synchronizedAttributes.getAttributes() + XCTAssertEqual(attributes.count, 2) + XCTAssertEqual(attributes["key1"] as? String, "value1") + XCTAssertEqual(attributes["key2"] as? String, "value2") + } + + func testThreadSafety() { + let synchronizedAttributes = SynchronizedAttributes(attributes: [:]) + + callConcurrently( + closures: [ + { idx in synchronizedAttributes.addAttribute(key: "key\(idx)", value: "value\(idx)") }, + { idx in synchronizedAttributes.removeAttribute(forKey: "unknown-key\(idx)") }, + { _ in _ = synchronizedAttributes.getAttributes() }, + ], + iterations: 1_000 + ) + + XCTAssertEqual(synchronizedAttributes.getAttributes().count, 1_000) + } +} diff --git a/DatadogLogs/Tests/Log/SynchronizedTagsTests.swift b/DatadogLogs/Tests/Log/SynchronizedTagsTests.swift new file mode 100644 index 0000000000..95c81ecc4c --- /dev/null +++ b/DatadogLogs/Tests/Log/SynchronizedTagsTests.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-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogLogs + +final class SynchronizedTagsTests: XCTestCase { + func testAddTag() { + let synchronizedTags = SynchronizedTags(tags: []) + synchronizedTags.addTag("tag1") + + let tags = synchronizedTags.getTags() + XCTAssertTrue(tags.contains("tag1")) + XCTAssertEqual(tags.count, 1) + } + + func testRemoveTag() { + let synchronizedTags = SynchronizedTags(tags: ["tag1", "tag2"]) + synchronizedTags.removeTag("tag1") + + let tags = synchronizedTags.getTags() + XCTAssertFalse(tags.contains("tag1")) + XCTAssertTrue(tags.contains("tag2")) + XCTAssertEqual(tags.count, 1) + } + + func testRemoveTagsWithPredicate() { + let synchronizedTags = SynchronizedTags(tags: ["tag1", "tag2", "tag3", "tag4"]) + synchronizedTags.removeTags { $0.contains("2") || $0.contains("4") } + + let tags = synchronizedTags.getTags() + XCTAssertFalse(tags.contains("tag2")) + XCTAssertFalse(tags.contains("tag4")) + XCTAssertTrue(tags.contains("tag1")) + XCTAssertTrue(tags.contains("tag3")) + XCTAssertEqual(tags.count, 2) + } + + func testGetTags() { + let initialTags: Set = ["tag1", "tag2"] + let synchronizedTags = SynchronizedTags(tags: initialTags) + + let tags = synchronizedTags.getTags() + XCTAssertEqual(tags, initialTags) + } + + func testThreadSafety() { + let synchronizedTags = SynchronizedTags(tags: []) + + callConcurrently( + closures: [ + { idx in synchronizedTags.addTag("tag\(idx)") }, + { idx in synchronizedTags.removeTag("unknown-tag\(idx)") }, + { idx in synchronizedTags.removeTags(where: { _ in false }) }, + { _ in _ = synchronizedTags.getTags() }, + ], + iterations: 1_000 + ) + + XCTAssertEqual(synchronizedTags.getTags().count, 1_000) + } +} diff --git a/DatadogLogs/Tests/LogsTests.swift b/DatadogLogs/Tests/LogsTests.swift index 72c202dfce..ba16749304 100644 --- a/DatadogLogs/Tests/LogsTests.swift +++ b/DatadogLogs/Tests/LogsTests.swift @@ -39,6 +39,18 @@ class LogsTests: XCTestCase { XCTAssertTrue(Logs._internal.isEnabled(in: core)) } + func testInitializedWithBacktraceReporter() throws { + // Given + let core = FeatureRegistrationCoreMock() + + // When + Logs.enable(in: core) + + // Then + let logs = try XCTUnwrap(core.get(feature: LogsFeature.self)) + XCTAssertNotNil(logs.backtraceReporter) + } + func testConfigurationOverrides() throws { // Given let customEndpoint: URL = .mockRandom() @@ -94,7 +106,7 @@ class LogsTests: XCTestCase { // Then let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) - XCTAssertEqual(feature.getAttributes()[attributeKey] as? String, attributeValue) + XCTAssertEqual(feature.attributes.getAttributes()[attributeKey] as? String, attributeValue) } func testLogsRemoveAttributeForwardedToFeature() throws { @@ -111,7 +123,7 @@ class LogsTests: XCTestCase { // Then let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) - XCTAssertNil(feature.getAttributes()[attributeKey]) + XCTAssertNil(feature.attributes.getAttributes()[attributeKey]) } func testItSendsGlobalLogUpdates_whenAddAttribute() throws { diff --git a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift index 8040d4f5e3..275a5837ee 100644 --- a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift +++ b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift @@ -332,3 +332,9 @@ extension LogEvent.Attributes: Equatable { && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) } } + +extension SynchronizedAttributes: AnyMockable { + public static func mockAny() -> SynchronizedAttributes { + return SynchronizedAttributes(attributes: [:]) + } +} diff --git a/DatadogLogs/Tests/RemoteLoggerTests.swift b/DatadogLogs/Tests/RemoteLoggerTests.swift index 9896fb042d..6b98bfd8c3 100644 --- a/DatadogLogs/Tests/RemoteLoggerTests.swift +++ b/DatadogLogs/Tests/RemoteLoggerTests.swift @@ -7,11 +7,14 @@ import XCTest import TestUtilities import DatadogInternal - @testable import DatadogLogs -private class ErrorMessageReceiverMock: FeatureMessageReceiver { - struct ErrorMessage: Decodable { +class RemoteLoggerTests: XCTestCase { + private let featureScope = FeatureScopeMock() + + // MARK: - Sending Error Message over Message Bus + + private struct ExpectedErrorMessage: Decodable { /// The Log error message let message: String /// The Log error type @@ -22,227 +25,317 @@ private class ErrorMessageReceiverMock: FeatureMessageReceiver { let source: String /// The Log attributes let attributes: [String: AnyCodable] + /// Binary images + let binaryImages: [BinaryImage]? } - var errors: [ErrorMessage] = [] - - /// Adds RUM Error with given message and stack to current RUM View. - func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { - guard - let error = try? message.baggage(forKey: "error", type: ErrorMessage.self) - else { - return false - } + func testWhenNonErrorLogged_itDoesNotPostsToMessageBus() throws { + // Given + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() + ) - self.errors.append(error) + // When + logger.info("Info message") - return true + // Then + XCTAssertEqual(featureScope.messagesSent().count, 0) } -} -class RemoteLoggerTests: XCTestCase { - func testItSendsErrorAlongWithErrorLog() throws { - let messageReceiver = ErrorMessageReceiverMock() - - let core = PassthroughCoreMock( - expectation: expectation(description: "Send error"), - messageReceiver: messageReceiver + func testWhenErrorLogged_itPostsToMessageBus() throws { + // Given + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() ) + // When + logger.error("Error message") + + // Then + let errorBaggage = try XCTUnwrap(featureScope.messagesSent().firstBaggage(withKey: "error")) + let error: ExpectedErrorMessage = try errorBaggage.decode() + XCTAssertEqual(error.message, "Error message") + } + + func testWhenCrossPlatformCrashErrorLogged_itDoesNotPostToMessageBus() throws { // Given let logger = RemoteLogger( - core: core, + featureScope: featureScope, + globalAttributes: .mockAny(), configuration: .mockAny(), dateProvider: RelativeDateProvider(), rumContextIntegration: false, - activeSpanIntegration: false + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() ) // When - logger.error("Error message") + logger.error("Error message", error: nil, attributes: [CrossPlatformAttributes.errorLogIsCrash: true]) // Then - waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertEqual(featureScope.messagesSent().count, 0) + } + + func testWhenAttributesContainIncludeBinaryImages_itPostsBinaryImagesToMessageBus() throws { + let stubBacktrace: BacktraceReport = .mockRandom() + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock(backtrace: stubBacktrace) + ) - XCTAssertEqual(messageReceiver.errors.count, 1) - XCTAssertEqual(messageReceiver.errors.first?.message, "Error message") + // When + logger.error("Information message", error: ErrorMock(), attributes: [CrossPlatformAttributes.includeBinaryImages: true]) + + // Then + let errorBaggage = try XCTUnwrap(featureScope.messagesSent().firstBaggage(withKey: "error")) + let error: ExpectedErrorMessage = try errorBaggage.decode() + // This is removed because binary images are sent in the message, so the additional attribute isn't needed + XCTAssertNil(error.attributes[CrossPlatformAttributes.includeBinaryImages]) + XCTAssertEqual(error.binaryImages?.count, stubBacktrace.binaryImages.count) + for i in 0.. DDLogEvent?) { + configuration.eventMapper = { swiftEvent in + let objcEvent = DDLogEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } } @objc @@ -191,7 +203,7 @@ public class DDLoggerConfiguration: NSObject { networkInfoEnabled: Bool = false, bundleWithRumEnabled: Bool = true, bundleWithTraceEnabled: Bool = true, - remoteSampleRate: Float = 100, + remoteSampleRate: SampleRate = .maxSampleRate, remoteLogThreshold: DDLogLevel = .debug, printLogsToConsole: Bool = false ) { @@ -225,12 +237,12 @@ public class DDLogger: NSObject { @objc public func debug(_ message: String, attributes: [String: Any]) { - sdkLogger.debug(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.debug(message, attributes: attributes.dd.swiftAttributes) } @objc public func debug(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.debug(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.debug(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc @@ -240,12 +252,12 @@ public class DDLogger: NSObject { @objc public func info(_ message: String, attributes: [String: Any]) { - sdkLogger.info(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.info(message, attributes: attributes.dd.swiftAttributes) } @objc public func info(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.info(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.info(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc @@ -255,12 +267,12 @@ public class DDLogger: NSObject { @objc public func notice(_ message: String, attributes: [String: Any]) { - sdkLogger.notice(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.notice(message, attributes: attributes.dd.swiftAttributes) } @objc public func notice(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.notice(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.notice(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc @@ -270,12 +282,12 @@ public class DDLogger: NSObject { @objc public func warn(_ message: String, attributes: [String: Any]) { - sdkLogger.warn(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.warn(message, attributes: attributes.dd.swiftAttributes) } @objc public func warn(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.warn(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.warn(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc @@ -285,12 +297,12 @@ public class DDLogger: NSObject { @objc public func error(_ message: String, attributes: [String: Any]) { - sdkLogger.error(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.error(message, attributes: attributes.dd.swiftAttributes) } @objc public func error(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.error(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.error(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc @@ -300,12 +312,12 @@ public class DDLogger: NSObject { @objc public func critical(_ message: String, attributes: [String: Any]) { - sdkLogger.critical(message, attributes: castAttributesToSwift(attributes)) + sdkLogger.critical(message, attributes: attributes.dd.swiftAttributes) } @objc public func critical(_ message: String, error: NSError, attributes: [String: Any]) { - sdkLogger.critical(message, error: error, attributes: castAttributesToSwift(attributes)) + sdkLogger.critical(message, error: error, attributes: attributes.dd.swiftAttributes) } @objc diff --git a/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift b/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift new file mode 100644 index 0000000000..7c8333bd24 --- /dev/null +++ b/DatadogObjc/Sources/Logs/LogsDataModels+objc.swift @@ -0,0 +1,486 @@ +/* + * 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 DatadogLogs +import DatadogInternal + +@objc +public class DDLogEvent: NSObject { + internal var swiftModel: LogEvent + + internal init(swiftModel: LogEvent) { + self.swiftModel = swiftModel + } + + @objc public var date: Date { + swiftModel.date + } + + @objc public var status: DDLogEventStatus { + .init(swift: swiftModel.status) + } + + @objc public var message: String { + set { swiftModel.message = newValue } + get { swiftModel.message } + } + + @objc public var error: DDLogEventError? { + if swiftModel.error != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var serviceName: String { + swiftModel.serviceName + } + + @objc public var environment: String { + swiftModel.environment + } + + @objc public var loggerName: String { + swiftModel.loggerName + } + + @objc public var loggerVersion: String { + swiftModel.loggerVersion + } + + @objc public var threadName: String? { + swiftModel.threadName + } + + @objc public var applicationVersion: String { + swiftModel.applicationVersion + } + + @objc public var applicationBuildNumber: String { + swiftModel.applicationBuildNumber + } + + @objc public var buildId: String? { + swiftModel.buildId + } + + @objc public var variant: String? { + swiftModel.variant + } + + @objc public var dd: DDLogEventDd { + .init(root: self) + } + + @objc public var os: DDLogEventOperatingSystem { + .init(root: self) + } + + @objc public var userInfo: DDLogEventUserInfo { + .init(root: self) + } + + @objc public var networkConnectionInfo: DDLogEventNetworkConnectionInfo? { + if swiftModel.networkConnectionInfo != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var mobileCarrierInfo: DDLogEventCarrierInfo? { + if swiftModel.mobileCarrierInfo != nil { + .init(root: self) + } else { + nil + } + } + + @objc public var attributes: DDLogEventAttributes { + .init(root: self) + } + + @objc public var tags: [String]? { + set { swiftModel.tags = newValue } + get { swiftModel.tags } + } +} + +@objc +public enum DDLogEventStatus: Int { + internal init(swift: LogEvent.Status) { + switch swift { + case .debug: self = .debug + case .info: self = .info + case .notice: self = .notice + case .warn: self = .warn + case .error: self = .error + case .critical: self = .critical + case .emergency: self = .emergency + } + } + + internal var toSwift: LogEvent.Status { + switch self { + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warn: return .warn + case .error: return .error + case .critical: return .critical + case .emergency: return .emergency + } + } + + case debug + case info + case notice + case warn + case error + case critical + case emergency +} + +@objc +public class DDLogEventAttributes: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var userAttributes: [String: Any] { + set { root.swiftModel.attributes.userAttributes = newValue.dd.swiftAttributes } + get { root.swiftModel.attributes.userAttributes.dd.objCAttributes } + } +} + +@objc +public class DDLogEventUserInfo: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var id: String? { + root.swiftModel.userInfo.id + } + + @objc public var name: String? { + root.swiftModel.userInfo.name + } + + @objc public var email: String? { + root.swiftModel.userInfo.email + } + + @objc public var extraInfo: [String: Any] { + set { root.swiftModel.userInfo.extraInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.userInfo.extraInfo.dd.objCAttributes } + } +} + +@objc +public class DDLogEventError: NSObject { + internal var root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var kind: String? { + set { root.swiftModel.error?.kind = newValue } + get { root.swiftModel.error?.kind } + } + + @objc public var message: String? { + set { root.swiftModel.error?.message = newValue } + get { root.swiftModel.error?.message } + } + + @objc public var stack: String? { + set { root.swiftModel.error?.stack = newValue } + get { root.swiftModel.error?.stack } + } + + @objc public var sourceType: String { + // swiftlint:disable force_unwrapping + set { root.swiftModel.error!.sourceType = newValue } + get { root.swiftModel.error!.sourceType } + // swiftlint:enable force_unwrapping + } + + @objc public var fingerprint: String? { + set { root.swiftModel.error?.fingerprint = newValue } + get { root.swiftModel.error?.fingerprint } + } + + @objc public var binaryImages: [DDLogEventBinaryImage]? { + set { root.swiftModel.error?.binaryImages = newValue?.map { $0.swiftModel } } + get { root.swiftModel.error?.binaryImages?.map { DDLogEventBinaryImage(swiftModel: $0) } } + } +} + +@objc +public class DDLogEventBinaryImage: NSObject { + internal let swiftModel: LogEvent.Error.BinaryImage + + internal init(swiftModel: LogEvent.Error.BinaryImage) { + self.swiftModel = swiftModel + } + + @objc public var arch: String? { + swiftModel.arch + } + + @objc public var isSystem: Bool { + swiftModel.isSystem + } + + @objc public var loadAddress: String? { + swiftModel.loadAddress + } + + @objc public var maxAddress: String? { + swiftModel.maxAddress + } + + @objc public var name: String { + swiftModel.name + } + + @objc public var uuid: String { + swiftModel.uuid + } +} + +@objc +public class DDLogEventOperatingSystem: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var name: String { + root.swiftModel.os.name + } + + @objc public var version: String { + root.swiftModel.os.version + } + + @objc public var build: String? { + root.swiftModel.os.build + } +} + +@objc +public class DDLogEventDd: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var device: DDLogEventDeviceInfo { + .init(root: root) + } +} + +@objc +public class DDLogEventDeviceInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var brand: String { + root.swiftModel.dd.device.brand + } + + @objc public var name: String { + root.swiftModel.dd.device.name + } + + @objc public var model: String { + root.swiftModel.dd.device.model + } + + @objc public var architecture: String { + root.swiftModel.dd.device.architecture + } +} + +@objc +public class DDLogEventNetworkConnectionInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var reachability: DDLogEventReachability { + // swiftlint:disable force_unwrapping + .init(swift: root.swiftModel.networkConnectionInfo!.reachability) + // swiftlint:enable force_unwrapping + } + + @objc public var availableInterfaces: [Int]? { + root.swiftModel.networkConnectionInfo?.availableInterfaces?.map { DDLogEventInterface(swift: $0).rawValue } + } + + @objc public var supportsIPv4: NSNumber? { + root.swiftModel.networkConnectionInfo?.supportsIPv4 as NSNumber? + } + + @objc public var supportsIPv6: NSNumber? { + root.swiftModel.networkConnectionInfo?.supportsIPv6 as NSNumber? + } + + @objc public var isExpensive: NSNumber? { + root.swiftModel.networkConnectionInfo?.isExpensive as NSNumber? + } + + @objc public var isConstrained: NSNumber? { + root.swiftModel.networkConnectionInfo?.isConstrained as NSNumber? + } +} + +@objc +public enum DDLogEventReachability: Int { + internal init(swift: NetworkConnectionInfo.Reachability) { + switch swift { + case .yes: self = .yes + case .maybe: self = .maybe + case .no: self = .no + } + } + + internal var toSwift: NetworkConnectionInfo.Reachability { + switch self { + case .yes: return .yes + case .maybe: return .maybe + case .no: return .no + } + } + + case yes + case maybe + case no +} + +@objc +public enum DDLogEventInterface: Int { + internal init(swift: NetworkConnectionInfo.Interface) { + switch swift { + case .wifi: self = .wifi + case .wiredEthernet: self = .wiredEthernet + case .cellular: self = .cellular + case .loopback: self = .loopback + case .other: self = .other + } + } + + internal var toSwift: NetworkConnectionInfo.Interface { + switch self { + case .wifi: return .wifi + case .wiredEthernet: return .wiredEthernet + case .cellular: return .cellular + case .loopback: return .loopback + case .other: return .other + } + } + + case wifi + case wiredEthernet + case cellular + case loopback + case other +} + +@objc +public class DDLogEventCarrierInfo: NSObject { + internal let root: DDLogEvent + + internal init(root: DDLogEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.mobileCarrierInfo?.carrierName + } + + @objc public var carrierISOCountryCode: String? { + root.swiftModel.mobileCarrierInfo?.carrierISOCountryCode + } + + @objc public var carrierAllowsVOIP: Bool { + // swiftlint:disable force_unwrapping + root.swiftModel.mobileCarrierInfo!.carrierAllowsVOIP + // swiftlint:enable force_unwrapping + } + + @objc public var radioAccessTechnology: DDLogEventRadioAccessTechnology { + // swiftlint:disable force_unwrapping + .init(swift: root.swiftModel.mobileCarrierInfo!.radioAccessTechnology) + // swiftlint:enable force_unwrapping + } +} + +@objc +public enum DDLogEventRadioAccessTechnology: Int { + internal init(swift: CarrierInfo.RadioAccessTechnology) { + switch swift { + case .GPRS: self = .GPRS + case .Edge: self = .Edge + case .WCDMA: self = .WCDMA + case .HSDPA: self = .HSDPA + case .HSUPA: self = .HSUPA + case .CDMA1x: self = .CDMA1x + case .CDMAEVDORev0: self = .CDMAEVDORev0 + case .CDMAEVDORevA: self = .CDMAEVDORevA + case .CDMAEVDORevB: self = .CDMAEVDORevB + case .eHRPD: self = .eHRPD + case .LTE: self = .LTE + case .unknown: self = .unknown + } + } + + internal var toSwift: CarrierInfo.RadioAccessTechnology { + switch self { + case .GPRS: return .GPRS + case .Edge: return .Edge + case .WCDMA: return .WCDMA + case .HSDPA: return .HSDPA + case .HSUPA: return .HSUPA + case .CDMA1x: return .CDMA1x + case .CDMAEVDORev0: return .CDMAEVDORev0 + case .CDMAEVDORevA: return .CDMAEVDORevA + case .CDMAEVDORevB: return .CDMAEVDORevB + case .eHRPD: return .eHRPD + case .LTE: return .LTE + case .unknown: return .unknown + } + } + + case GPRS + case Edge + case WCDMA + case HSDPA + case HSUPA + case CDMA1x + case CDMAEVDORev0 + case CDMAEVDORevA + case CDMAEVDORevB + case eHRPD + case LTE + case unknown +} diff --git a/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift b/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift deleted file mode 100644 index a5a3f160f9..0000000000 --- a/DatadogObjc/Sources/ObjcIntercompatibility/ObjcIntercompatibility.swift +++ /dev/null @@ -1,28 +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 DatadogInternal -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) } -} - -/// 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 } -} - -/// 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) - } -} diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift index 6f2e8cd113..26b651ee78 100644 --- a/DatadogObjc/Sources/RUM/RUM+objc.swift +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -22,7 +22,7 @@ public class DDRUMView: NSObject { let swiftView: RUMView @objc public var name: String { swiftView.name } - @objc public var attributes: [String: Any] { castAttributesToObjectiveC(swiftView.attributes) } + @objc public var attributes: [String: Any] { swiftView.attributes.dd.objCAttributes } /// Initializes the RUM View description. /// - Parameters: @@ -32,7 +32,7 @@ public class DDRUMView: NSObject { public init(name: String, attributes: [String: Any]) { swiftView = RUMView( name: name, - attributes: castAttributesToSwift(attributes) + attributes: attributes.dd.swiftAttributes ) } } @@ -51,7 +51,7 @@ public class DDDefaultUIKitRUMViewsPredicate: NSObject, DDUIKitRUMViewsPredicate public func rumView(for viewController: UIViewController) -> DDRUMView? { return swiftPredicate.rumView(for: viewController).map { - DDRUMView(name: $0.name, attributes: castAttributesToObjectiveC($0.attributes)) + DDRUMView(name: $0.name, attributes: $0.attributes.dd.objCAttributes) } } } @@ -62,13 +62,13 @@ public class DDDefaultUIKitRUMActionsPredicate: NSObject, DDUIKitRUMActionsPredi #if os(tvOS) public func rumAction(press type: UIPress.PressType, targetView: UIView) -> DDRUMAction? { swiftPredicate.rumAction(press: type, targetView: targetView).map { - DDRUMAction(name: $0.name, attributes: castAttributesToObjectiveC($0.attributes)) + DDRUMAction(name: $0.name, attributes: $0.attributes.dd.objCAttributes) } } #else public func rumAction(targetView: UIView) -> DDRUMAction? { swiftPredicate.rumAction(targetView: targetView).map { - DDRUMAction(name: $0.name, attributes: castAttributesToObjectiveC($0.attributes)) + DDRUMAction(name: $0.name, attributes: $0.attributes.dd.objCAttributes) } } #endif @@ -105,7 +105,7 @@ public class DDRUMAction: NSObject { let swiftAction: RUMAction @objc public var name: String { swiftAction.name } - @objc public var attributes: [String: Any] { castAttributesToObjectiveC(swiftAction.attributes) } + @objc public var attributes: [String: Any] { swiftAction.attributes.dd.objCAttributes } /// Initializes the RUM Action description. /// - Parameters: @@ -115,7 +115,7 @@ public class DDRUMAction: NSObject { public init(name: String, attributes: [String: Any]) { swiftAction = RUMAction( name: name, - attributes: castAttributesToSwift(attributes) + attributes: attributes.dd.swiftAttributes ) } } @@ -319,7 +319,7 @@ public class DDRUMURLSessionTracking: NSObject { public func setResourceAttributesProvider(_ provider: @escaping (URLRequest, URLResponse?, Data?, Error?) -> [String: Any]?) { swiftConfig.resourceAttributesProvider = { request, response, data, error in let objcAttributes = provider(request, response, data, error) - return objcAttributes.map { castAttributesToSwift($0) } + return objcAttributes?.dd.swiftAttributes } } } @@ -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 } } @@ -479,7 +484,7 @@ public class DDRUMMonitor: NSObject { name: String?, attributes: [String: Any] ) { - swiftRUMMonitor.startView(viewController: viewController, name: name, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startView(viewController: viewController, name: name, attributes: attributes.dd.swiftAttributes) } @objc @@ -487,7 +492,7 @@ public class DDRUMMonitor: NSObject { viewController: UIViewController, attributes: [String: Any] ) { - swiftRUMMonitor.stopView(viewController: viewController, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopView(viewController: viewController, attributes: attributes.dd.swiftAttributes) } @objc @@ -496,7 +501,7 @@ public class DDRUMMonitor: NSObject { name: String?, attributes: [String: Any] ) { - swiftRUMMonitor.startView(key: key, name: name, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startView(key: key, name: name, attributes: attributes.dd.swiftAttributes) } @objc @@ -504,7 +509,7 @@ public class DDRUMMonitor: NSObject { key: String, attributes: [String: Any] ) { - swiftRUMMonitor.stopView(key: key, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopView(key: key, attributes: attributes.dd.swiftAttributes) } @objc @@ -519,7 +524,7 @@ public class DDRUMMonitor: NSObject { source: DDRUMErrorSource, attributes: [String: Any] ) { - swiftRUMMonitor.addError(message: message, stack: stack, source: source.swiftType, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.addError(message: message, stack: stack, source: source.swiftType, attributes: attributes.dd.swiftAttributes) } @objc @@ -528,7 +533,7 @@ public class DDRUMMonitor: NSObject { source: DDRUMErrorSource, attributes: [String: Any] ) { - swiftRUMMonitor.addError(error: error, source: source.swiftType, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.addError(error: error, source: source.swiftType, attributes: attributes.dd.swiftAttributes) } @objc @@ -537,7 +542,7 @@ public class DDRUMMonitor: NSObject { request: URLRequest, attributes: [String: Any] ) { - swiftRUMMonitor.startResource(resourceKey: resourceKey, request: request, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startResource(resourceKey: resourceKey, request: request, attributes: attributes.dd.swiftAttributes) } @objc @@ -546,7 +551,7 @@ public class DDRUMMonitor: NSObject { url: URL, attributes: [String: Any] ) { - swiftRUMMonitor.startResource(resourceKey: resourceKey, url: url, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startResource(resourceKey: resourceKey, url: url, attributes: attributes.dd.swiftAttributes) } @objc @@ -556,7 +561,7 @@ public class DDRUMMonitor: NSObject { urlString: String, attributes: [String: Any] ) { - swiftRUMMonitor.startResource(resourceKey: resourceKey, httpMethod: httpMethod.swiftType, urlString: urlString, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startResource(resourceKey: resourceKey, httpMethod: httpMethod.swiftType, urlString: urlString, attributes: attributes.dd.swiftAttributes) } @objc @@ -565,7 +570,7 @@ public class DDRUMMonitor: NSObject { metrics: URLSessionTaskMetrics, attributes: [String: Any] ) { - swiftRUMMonitor.addResourceMetrics(resourceKey: resourceKey, metrics: metrics, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.addResourceMetrics(resourceKey: resourceKey, metrics: metrics, attributes: attributes.dd.swiftAttributes) } @objc @@ -575,7 +580,7 @@ public class DDRUMMonitor: NSObject { size: NSNumber?, attributes: [String: Any] ) { - swiftRUMMonitor.stopResource(resourceKey: resourceKey, response: response, size: size?.int64Value, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopResource(resourceKey: resourceKey, response: response, size: size?.int64Value, attributes: attributes.dd.swiftAttributes) } @objc @@ -591,7 +596,7 @@ public class DDRUMMonitor: NSObject { statusCode: statusCode?.intValue, kind: kind.swiftType, size: size?.int64Value, - attributes: castAttributesToSwift(attributes) + attributes: attributes.dd.swiftAttributes ) } @@ -602,7 +607,7 @@ public class DDRUMMonitor: NSObject { response: URLResponse?, attributes: [String: Any] ) { - swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, error: error, response: response, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, error: error, response: response, attributes: attributes.dd.swiftAttributes) } @objc @@ -612,7 +617,7 @@ public class DDRUMMonitor: NSObject { response: URLResponse?, attributes: [String: Any] ) { - swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, message: message, response: response, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, message: message, response: response, attributes: attributes.dd.swiftAttributes) } @objc @@ -621,7 +626,7 @@ public class DDRUMMonitor: NSObject { name: String, attributes: [String: Any] ) { - swiftRUMMonitor.startAction(type: type.swiftType, name: name, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.startAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) } @objc @@ -630,7 +635,7 @@ public class DDRUMMonitor: NSObject { name: String?, attributes: [String: Any] ) { - swiftRUMMonitor.stopAction(type: type.swiftType, name: name, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.stopAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) } @objc @@ -639,7 +644,7 @@ public class DDRUMMonitor: NSObject { name: String, attributes: [String: Any] ) { - swiftRUMMonitor.addAction(type: type.swiftType, name: name, attributes: castAttributesToSwift(attributes)) + swiftRUMMonitor.addAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) } @objc diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 283d4b9530..a39cf11248 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -142,6 +142,11 @@ public class DDRUMActionEventDDAction: NSObject { self.root = root } + @objc public var nameSource: DDRUMActionEventDDActionNameSource { + set { root.swiftModel.dd.action!.nameSource = newValue.toSwift } + get { .init(swift: root.swiftModel.dd.action!.nameSource) } + } + @objc public var position: DDRUMActionEventDDActionPosition? { root.swiftModel.dd.action!.position != nil ? DDRUMActionEventDDActionPosition(root: root) : nil } @@ -151,6 +156,41 @@ public class DDRUMActionEventDDAction: NSObject { } } +@objc +public enum DDRUMActionEventDDActionNameSource: Int { + internal init(swift: RUMActionEvent.DD.Action.NameSource?) { + switch swift { + case nil: self = .none + case .customAttribute?: self = .customAttribute + case .maskPlaceholder?: self = .maskPlaceholder + case .standardAttribute?: self = .standardAttribute + case .textContent?: self = .textContent + case .maskDisallowed?: self = .maskDisallowed + case .blank?: self = .blank + } + } + + internal var toSwift: RUMActionEvent.DD.Action.NameSource? { + switch self { + case .none: return nil + case .customAttribute: return .customAttribute + case .maskPlaceholder: return .maskPlaceholder + case .standardAttribute: return .standardAttribute + case .textContent: return .textContent + case .maskDisallowed: return .maskDisallowed + case .blank: return .blank + } + } + + case none + case customAttribute + case maskPlaceholder + case standardAttribute + case textContent + case maskDisallowed + case blank +} + @objc public class DDRUMActionEventDDActionPosition: NSObject { internal let root: DDRUMActionEvent @@ -664,6 +704,7 @@ public enum DDRUMActionEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -676,6 +717,7 @@ public enum DDRUMActionEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -686,6 +728,7 @@ public enum DDRUMActionEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -710,7 +753,8 @@ public class DDRUMActionEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -889,6 +933,7 @@ public enum DDRUMActionEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -902,6 +947,7 @@ public enum DDRUMActionEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -913,6 +959,7 @@ public enum DDRUMActionEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -944,6 +991,10 @@ public class DDRUMActionEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -957,7 +1008,8 @@ public class DDRUMActionEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -1447,6 +1499,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -1459,6 +1512,7 @@ public enum DDRUMErrorEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -1469,6 +1523,7 @@ public enum DDRUMErrorEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -1493,7 +1548,8 @@ public class DDRUMErrorEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -1722,6 +1778,8 @@ public enum DDRUMErrorEventErrorCategory: Int { case .aNR?: self = .aNR case .appHang?: self = .appHang case .exception?: self = .exception + case .watchdogTermination?: self = .watchdogTermination + case .memoryWarning?: self = .memoryWarning } } @@ -1731,6 +1789,8 @@ public enum DDRUMErrorEventErrorCategory: Int { case .aNR: return .aNR case .appHang: return .appHang case .exception: return .exception + case .watchdogTermination: return .watchdogTermination + case .memoryWarning: return .memoryWarning } } @@ -1738,6 +1798,8 @@ public enum DDRUMErrorEventErrorCategory: Int { case aNR case appHang case exception + case watchdogTermination + case memoryWarning } @objc @@ -2166,7 +2228,8 @@ public class DDRUMErrorEventFeatureFlags: NSObject { } @objc public var featureFlagsInfo: [String: Any] { - root.swiftModel.featureFlags!.featureFlagsInfo.castToObjectiveC() + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } } } @@ -2264,6 +2327,7 @@ public enum DDRUMErrorEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -2277,6 +2341,7 @@ public enum DDRUMErrorEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -2288,6 +2353,7 @@ public enum DDRUMErrorEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -2319,6 +2385,10 @@ public class DDRUMErrorEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -2332,7 +2402,8 @@ public class DDRUMErrorEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -2818,6 +2889,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -2830,6 +2902,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -2840,6 +2913,7 @@ public enum DDRUMLongTaskEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -2864,7 +2938,8 @@ public class DDRUMLongTaskEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -2970,10 +3045,22 @@ public class DDRUMLongTaskEventLongTask: NSObject { self.root = root } + @objc public var blockingDuration: NSNumber? { + root.swiftModel.longTask.blockingDuration as NSNumber? + } + @objc public var duration: NSNumber { root.swiftModel.longTask.duration as NSNumber } + @objc public var entryType: DDRUMLongTaskEventLongTaskEntryType { + .init(swift: root.swiftModel.longTask.entryType) + } + + @objc public var firstUiEventTimestamp: NSNumber? { + root.swiftModel.longTask.firstUiEventTimestamp as NSNumber? + } + @objc public var id: String? { root.swiftModel.longTask.id } @@ -2981,6 +3068,134 @@ public class DDRUMLongTaskEventLongTask: NSObject { @objc public var isFrozenFrame: NSNumber? { root.swiftModel.longTask.isFrozenFrame as NSNumber? } + + @objc public var renderStart: NSNumber? { + root.swiftModel.longTask.renderStart as NSNumber? + } + + @objc public var scripts: [DDRUMLongTaskEventLongTaskScripts]? { + root.swiftModel.longTask.scripts?.map { DDRUMLongTaskEventLongTaskScripts(swiftModel: $0) } + } + + @objc public var startTime: NSNumber? { + root.swiftModel.longTask.startTime as NSNumber? + } + + @objc public var styleAndLayoutStart: NSNumber? { + root.swiftModel.longTask.styleAndLayoutStart as NSNumber? + } +} + +@objc +public enum DDRUMLongTaskEventLongTaskEntryType: Int { + internal init(swift: RUMLongTaskEvent.LongTask.EntryType?) { + switch swift { + case nil: self = .none + case .longTask?: self = .longTask + case .longAnimationFrame?: self = .longAnimationFrame + } + } + + internal var toSwift: RUMLongTaskEvent.LongTask.EntryType? { + switch self { + case .none: return nil + case .longTask: return .longTask + case .longAnimationFrame: return .longAnimationFrame + } + } + + case none + case longTask + case longAnimationFrame +} + +@objc +public class DDRUMLongTaskEventLongTaskScripts: NSObject { + internal var swiftModel: RUMLongTaskEvent.LongTask.Scripts + internal var root: DDRUMLongTaskEventLongTaskScripts { self } + + internal init(swiftModel: RUMLongTaskEvent.LongTask.Scripts) { + self.swiftModel = swiftModel + } + + @objc public var duration: NSNumber? { + root.swiftModel.duration as NSNumber? + } + + @objc public var executionStart: NSNumber? { + root.swiftModel.executionStart as NSNumber? + } + + @objc public var forcedStyleAndLayoutDuration: NSNumber? { + root.swiftModel.forcedStyleAndLayoutDuration as NSNumber? + } + + @objc public var invoker: String? { + root.swiftModel.invoker + } + + @objc public var invokerType: DDRUMLongTaskEventLongTaskScriptsInvokerType { + .init(swift: root.swiftModel.invokerType) + } + + @objc public var pauseDuration: NSNumber? { + root.swiftModel.pauseDuration as NSNumber? + } + + @objc public var sourceCharPosition: NSNumber? { + root.swiftModel.sourceCharPosition as NSNumber? + } + + @objc public var sourceFunctionName: String? { + root.swiftModel.sourceFunctionName + } + + @objc public var sourceUrl: String? { + root.swiftModel.sourceUrl + } + + @objc public var startTime: NSNumber? { + root.swiftModel.startTime as NSNumber? + } + + @objc public var windowAttribution: String? { + root.swiftModel.windowAttribution + } +} + +@objc +public enum DDRUMLongTaskEventLongTaskScriptsInvokerType: Int { + internal init(swift: RUMLongTaskEvent.LongTask.Scripts.InvokerType?) { + switch swift { + case nil: self = .none + case .userCallback?: self = .userCallback + case .eventListener?: self = .eventListener + case .resolvePromise?: self = .resolvePromise + case .rejectPromise?: self = .rejectPromise + case .classicScript?: self = .classicScript + case .moduleScript?: self = .moduleScript + } + } + + internal var toSwift: RUMLongTaskEvent.LongTask.Scripts.InvokerType? { + switch self { + case .none: return nil + case .userCallback: return .userCallback + case .eventListener: return .eventListener + case .resolvePromise: return .resolvePromise + case .rejectPromise: return .rejectPromise + case .classicScript: return .classicScript + case .moduleScript: return .moduleScript + } + } + + case none + case userCallback + case eventListener + case resolvePromise + case rejectPromise + case classicScript + case moduleScript } @objc @@ -3064,6 +3279,7 @@ public enum DDRUMLongTaskEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -3077,6 +3293,7 @@ public enum DDRUMLongTaskEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -3088,6 +3305,7 @@ public enum DDRUMLongTaskEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -3119,6 +3337,10 @@ public class DDRUMLongTaskEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -3132,7 +3354,8 @@ public class DDRUMLongTaskEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -3626,6 +3849,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -3638,6 +3862,7 @@ public enum DDRUMResourceEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -3648,6 +3873,7 @@ public enum DDRUMResourceEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -3672,7 +3898,8 @@ public class DDRUMResourceEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -3811,6 +4038,10 @@ public class DDRUMResourceEventResource: NSObject { root.swiftModel.resource.decodedBodySize as NSNumber? } + @objc public var deliveryType: DDRUMResourceEventResourceDeliveryType { + .init(swift: root.swiftModel.resource.deliveryType) + } + @objc public var dns: DDRUMResourceEventResourceDNS? { root.swiftModel.resource.dns != nil ? DDRUMResourceEventResourceDNS(root: root) : nil } @@ -3843,6 +4074,10 @@ public class DDRUMResourceEventResource: NSObject { .init(swift: root.swiftModel.resource.method) } + @objc public var `protocol`: String? { + root.swiftModel.resource.protocol + } + @objc public var provider: DDRUMResourceEventResourceProvider? { root.swiftModel.resource.provider != nil ? DDRUMResourceEventResourceProvider(root: root) : nil } @@ -3879,6 +4114,10 @@ public class DDRUMResourceEventResource: NSObject { set { root.swiftModel.resource.url = newValue } get { root.swiftModel.resource.url } } + + @objc public var worker: DDRUMResourceEventResourceWorker? { + root.swiftModel.resource.worker != nil ? DDRUMResourceEventResourceWorker(root: root) : nil + } } @objc @@ -3898,6 +4137,32 @@ public class DDRUMResourceEventResourceConnect: NSObject { } } +@objc +public enum DDRUMResourceEventResourceDeliveryType: Int { + internal init(swift: RUMResourceEvent.Resource.DeliveryType?) { + switch swift { + case nil: self = .none + case .cache?: self = .cache + case .navigationalPrefetch?: self = .navigationalPrefetch + case .other?: self = .other + } + } + + internal var toSwift: RUMResourceEvent.Resource.DeliveryType? { + switch self { + case .none: return nil + case .cache: return .cache + case .navigationalPrefetch: return .navigationalPrefetch + case .other: return .other + } + } + + case none + case cache + case navigationalPrefetch + case other +} + @objc public class DDRUMResourceEventResourceDNS: NSObject { internal let root: DDRUMResourceEvent @@ -4227,6 +4492,23 @@ public enum DDRUMResourceEventResourceResourceType: Int { case native } +@objc +public class DDRUMResourceEventResourceWorker: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.worker!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.worker!.start as NSNumber + } +} + @objc public class DDRUMResourceEventSession: NSObject { internal let root: DDRUMResourceEvent @@ -4283,6 +4565,7 @@ public enum DDRUMResourceEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -4296,6 +4579,7 @@ public enum DDRUMResourceEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -4307,6 +4591,7 @@ public enum DDRUMResourceEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -4338,6 +4623,10 @@ public class DDRUMResourceEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -4351,7 +4640,8 @@ public class DDRUMResourceEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -4877,6 +5167,7 @@ public enum DDRUMViewEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -4889,6 +5180,7 @@ public enum DDRUMViewEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -4899,6 +5191,7 @@ public enum DDRUMViewEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -4923,7 +5216,8 @@ public class DDRUMViewEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -5059,7 +5353,8 @@ public class DDRUMViewEventFeatureFlags: NSObject { } @objc public var featureFlagsInfo: [String: Any] { - root.swiftModel.featureFlags!.featureFlagsInfo.castToObjectiveC() + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } } } @@ -5188,6 +5483,7 @@ public enum DDRUMViewEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -5201,6 +5497,7 @@ public enum DDRUMViewEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -5212,6 +5509,7 @@ public enum DDRUMViewEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -5243,6 +5541,10 @@ public class DDRUMViewEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -5256,7 +5558,8 @@ public class DDRUMViewEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -5372,6 +5675,10 @@ public class DDRUMViewEventView: NSObject { root.swiftModel.view.interactionToNextPaintTime as NSNumber? } + @objc public var interactionToNextViewTime: NSNumber? { + root.swiftModel.view.interactionToNextViewTime as NSNumber? + } + @objc public var isActive: NSNumber? { root.swiftModel.view.isActive as NSNumber? } @@ -5421,6 +5728,10 @@ public class DDRUMViewEventView: NSObject { get { root.swiftModel.view.name } } + @objc public var networkSettledTime: NSNumber? { + root.swiftModel.view.networkSettledTime as NSNumber? + } + @objc public var referrer: String? { set { root.swiftModel.view.referrer = newValue } get { root.swiftModel.view.referrer } @@ -6096,6 +6407,7 @@ public enum DDRUMVitalEventContainerSource: Int { case .reactNative: self = .reactNative case .roku: self = .roku case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6108,6 +6420,7 @@ public enum DDRUMVitalEventContainerSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6118,6 +6431,7 @@ public enum DDRUMVitalEventContainerSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -6142,7 +6456,8 @@ public class DDRUMVitalEventRUMEventAttributes: NSObject { } @objc public var contextInfo: [String: Any] { - root.swiftModel.context!.contextInfo.castToObjectiveC() + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } } } @@ -6321,6 +6636,7 @@ public enum DDRUMVitalEventSource: Int { case .reactNative?: self = .reactNative case .roku?: self = .roku case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform } } @@ -6334,6 +6650,7 @@ public enum DDRUMVitalEventSource: Int { case .reactNative: return .reactNative case .roku: return .roku case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6345,6 +6662,7 @@ public enum DDRUMVitalEventSource: Int { case reactNative case roku case unity + case kotlinMultiplatform } @objc @@ -6376,6 +6694,10 @@ public class DDRUMVitalEventRUMUser: NSObject { self.root = root } + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + @objc public var email: String? { root.swiftModel.usr!.email } @@ -6389,7 +6711,8 @@ public class DDRUMVitalEventRUMUser: NSObject { } @objc public var usrInfo: [String: Any] { - root.swiftModel.usr!.usrInfo.castToObjectiveC() + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } } } @@ -6433,6 +6756,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 } @@ -6488,6 +6819,10 @@ public class DDTelemetryErrorEvent: NSObject { root.swiftModel.date as NSNumber } + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + @objc public var experimentalFeatures: [String]? { root.swiftModel.experimentalFeatures } @@ -6583,6 +6918,7 @@ public enum DDTelemetryErrorEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6594,6 +6930,7 @@ public enum DDTelemetryErrorEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6603,6 +6940,7 @@ public enum DDTelemetryErrorEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -6638,7 +6976,8 @@ public class DDTelemetryErrorEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.castToObjectiveC() + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -6739,6 +7078,10 @@ public class DDTelemetryDebugEvent: NSObject { root.swiftModel.date as NSNumber } + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + @objc public var experimentalFeatures: [String]? { root.swiftModel.experimentalFeatures } @@ -6834,6 +7177,7 @@ public enum DDTelemetryDebugEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -6845,6 +7189,7 @@ public enum DDTelemetryDebugEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -6854,6 +7199,7 @@ public enum DDTelemetryDebugEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -6885,7 +7231,8 @@ public class DDTelemetryDebugEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.castToObjectiveC() + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -6969,6 +7316,10 @@ public class DDTelemetryConfigurationEvent: NSObject { root.swiftModel.date as NSNumber } + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + @objc public var experimentalFeatures: [String]? { root.swiftModel.experimentalFeatures } @@ -7064,6 +7415,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .flutter: self = .flutter case .reactNative: self = .reactNative case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform } } @@ -7075,6 +7427,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case .flutter: return .flutter case .reactNative: return .reactNative case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform } } @@ -7084,6 +7437,7 @@ public enum DDTelemetryConfigurationEventSource: Int { case flutter case reactNative case unity + case kotlinMultiplatform } @objc @@ -7111,7 +7465,8 @@ public class DDTelemetryConfigurationEventTelemetry: NSObject { } @objc public var telemetryInfo: [String: Any] { - root.swiftModel.telemetry.telemetryInfo.castToObjectiveC() + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } } } @@ -7155,6 +7510,10 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.batchUploadFrequency as NSNumber? } + @objc public var collectFeatureFlagsOn: [Int]? { + root.swiftModel.telemetry.configuration.collectFeatureFlagsOn?.map { DDTelemetryConfigurationEventTelemetryConfigurationCollectFeatureFlagsOn(swift: $0).rawValue } + } + @objc public var compressIntakeRequests: NSNumber? { root.swiftModel.telemetry.configuration.compressIntakeRequests as NSNumber? } @@ -7186,16 +7545,30 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.forwardReports != nil ? DDTelemetryConfigurationEventTelemetryConfigurationForwardReports(root: root) : nil } + @objc public var imagePrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.imagePrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.imagePrivacyLevel } + } + @objc public var initializationType: String? { set { root.swiftModel.telemetry.configuration.initializationType = newValue } get { root.swiftModel.telemetry.configuration.initializationType } } + @objc public var isMainProcess: NSNumber? { + root.swiftModel.telemetry.configuration.isMainProcess as NSNumber? + } + @objc public var mobileVitalsUpdatePeriod: NSNumber? { set { root.swiftModel.telemetry.configuration.mobileVitalsUpdatePeriod = newValue?.int64Value } get { root.swiftModel.telemetry.configuration.mobileVitalsUpdatePeriod as NSNumber? } } + @objc public var plugins: [DDTelemetryConfigurationEventTelemetryConfigurationPlugins]? { + set { root.swiftModel.telemetry.configuration.plugins = newValue?.map { $0.swiftModel } } + get { root.swiftModel.telemetry.configuration.plugins?.map { DDTelemetryConfigurationEventTelemetryConfigurationPlugins(swiftModel: $0) } } + } + @objc public var premiumSampleRate: NSNumber? { root.swiftModel.telemetry.configuration.premiumSampleRate as NSNumber? } @@ -7236,6 +7609,11 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.silentMultipleInit as NSNumber? } + @objc public var startRecordingImmediately: NSNumber? { + set { root.swiftModel.telemetry.configuration.startRecordingImmediately = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.startRecordingImmediately as NSNumber? } + } + @objc public var startSessionReplayRecordingManually: NSNumber? { set { root.swiftModel.telemetry.configuration.startSessionReplayRecordingManually = newValue?.boolValue } get { root.swiftModel.telemetry.configuration.startSessionReplayRecordingManually as NSNumber? } @@ -7257,6 +7635,16 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { root.swiftModel.telemetry.configuration.telemetryUsageSampleRate as NSNumber? } + @objc public var textAndInputPrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.textAndInputPrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.textAndInputPrivacyLevel } + } + + @objc public var touchPrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.touchPrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.touchPrivacyLevel } + } + @objc public var traceContextInjection: DDTelemetryConfigurationEventTelemetryConfigurationTraceContextInjection { set { root.swiftModel.telemetry.configuration.traceContextInjection = newValue.toSwift } get { .init(swift: root.swiftModel.telemetry.configuration.traceContextInjection) } @@ -7419,6 +7807,32 @@ public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { } } +@objc +public enum DDTelemetryConfigurationEventTelemetryConfigurationCollectFeatureFlagsOn: Int { + internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.CollectFeatureFlagsOn?) { + switch swift { + case nil: self = .none + case .view?: self = .view + case .error?: self = .error + case .vital?: self = .vital + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.CollectFeatureFlagsOn? { + switch self { + case .none: return nil + case .view: return .view + case .error: return .error + case .vital: return .vital + } + } + + case none + case view + case error + case vital +} + @objc public class DDTelemetryConfigurationEventTelemetryConfigurationForwardConsoleLogs: NSObject { internal let root: DDTelemetryConfigurationEvent @@ -7465,6 +7879,25 @@ 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] { + set { root.swiftModel.pluginsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.pluginsInfo.dd.objCAttributes } + } +} + @objc public enum DDTelemetryConfigurationEventTelemetryConfigurationSelectedTracingPropagators: Int { internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.SelectedTracingPropagators?) { @@ -7629,4 +8062,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/81c3d7401cba2a2faf48b5f4c0e8aca05c759662 diff --git a/DatadogObjc/Sources/SessionReplay/SessionReplay+objc.swift b/DatadogObjc/Sources/SessionReplay/SessionReplay+objc.swift deleted file mode 100644 index 66e1285545..0000000000 --- a/DatadogObjc/Sources/SessionReplay/SessionReplay+objc.swift +++ /dev/null @@ -1,112 +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 os(iOS) - -import DatadogInternal -import DatadogSessionReplay - -/// An entry point to Datadog Session Replay feature. -@objc -@available(*, deprecated, message: "Obj-C API for Session Replay was moved to the DatadogSessionReplay package.") -public final class DDSessionReplay: NSObject { - override private init() { } - - /// Enables Datadog Session Replay feature. - /// - /// Recording will start automatically after enabling Session Replay. - /// - /// Note: Session Replay requires the RUM feature to be enabled. - /// - /// - Parameters: - /// - configuration: Configuration of the feature. - @objc - public static func enable(with configuration: DDSessionReplayConfiguration) { - SessionReplay.enable(with: configuration._swift) - } -} - -/// Session Replay feature configuration. -@objc -public final class DDSessionReplayConfiguration: NSObject { - internal var _swift: SessionReplay.Configuration = .init(replaySampleRate: 0) - - /// The sampling rate for Session Replay. It is applied in addition to the RUM session sample rate. - /// - /// It must be a number between 0.0 and 100.0, where 0 means no replays will be recorded - /// and 100 means all RUM sessions will contain replay. - /// - /// Note: This sample rate is applied in addition to the RUM sample rate. For example, if RUM uses a sample rate of 80% - /// and Session Replay uses a sample rate of 20%, it means that out of all user sessions, 80% will be included in RUM, - /// and within those sessions, only 20% will have replays. - @objc public var replaySampleRate: Float { - set { _swift.replaySampleRate = newValue } - get { _swift.replaySampleRate } - } - - /// Defines the way sensitive content (e.g. text) should be masked. - /// - /// Default: `.mask`. - @objc public var defaultPrivacyLevel: DDSessionReplayConfigurationPrivacyLevel { - set { _swift.defaultPrivacyLevel = newValue._swift } - get { .init(_swift.defaultPrivacyLevel) } - } - - /// Custom server url for sending replay data. - /// - /// Default: `nil`. - @objc public var customEndpoint: URL? { - set { _swift.customEndpoint = newValue } - get { _swift.customEndpoint } - } - - /// Creates Session Replay configuration. - /// - /// - Parameters: - /// - replaySampleRate: The sampling rate for Session Replay. It is applied in addition to the RUM session sample rate. - @objc - public required init( - replaySampleRate: Float - ) { - _swift = SessionReplay.Configuration( - replaySampleRate: replaySampleRate - ) - super.init() - } -} - -/// Available privacy levels for content masking. -@objc -public enum DDSessionReplayConfigurationPrivacyLevel: Int { - /// Record all content. - case allow - - /// Mask all content. - case mask - - /// Mask input elements, but record all other content. - case maskUserInput - - internal var _swift: SessionReplayPrivacyLevel { - switch self { - case .allow: return .allow - case .mask: return .mask - case .maskUserInput: return .maskUserInput - default: return .mask - } - } - - internal init(_ swift: SessionReplayPrivacyLevel) { - switch swift { - case .allow: self = .allow - case .mask: self = .mask - case .maskUserInput: self = .maskUserInput - } - } -} - -#endif diff --git a/DatadogObjc/Sources/Tracing/Trace+objc.swift b/DatadogObjc/Sources/Tracing/Trace+objc.swift index df3922f091..612c2ce4d3 100644 --- a/DatadogObjc/Sources/Tracing/Trace+objc.swift +++ b/DatadogObjc/Sources/Tracing/Trace+objc.swift @@ -28,8 +28,8 @@ public class DDTraceConfiguration: NSObject { } @objc public var tags: [String: Any]? { - set { swiftConfig.tags = newValue.map { castAttributesToSwift($0) } } - get { swiftConfig.tags.map { castAttributesToObjectiveC($0) } } + set { swiftConfig.tags = newValue?.dd.swiftAttributes } + get { swiftConfig.tags?.dd.objCAttributes } } @objc diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec index ae25d3ddcf..4992c12dec 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.22.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..ecdb28fe44 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -13,7 +13,7 @@ internal protocol RUMDataModel: Codable {} /// Schema of all properties of an Action event public struct RUMActionEvent: RUMDataModel { /// Internal properties - public let dd: DD + public var dd: DD /// Action properties public var action: Action @@ -37,7 +37,7 @@ public struct RUMActionEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -67,7 +67,7 @@ public struct RUMActionEvent: RUMDataModel { public let type: String = "action" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -102,7 +102,7 @@ public struct RUMActionEvent: RUMDataModel { /// Internal properties public struct DD: Codable { /// Action properties - public let action: Action? + public var action: Action? /// Browser SDK version public let browserSdkVersion: String? @@ -126,6 +126,9 @@ public struct RUMActionEvent: RUMDataModel { /// Action properties public struct Action: Codable { + /// The strategy of how the auto click action name is computed + public var nameSource: NameSource? + /// Action position properties public let position: Position? @@ -133,10 +136,21 @@ public struct RUMActionEvent: RUMDataModel { public let target: Target? enum CodingKeys: String, CodingKey { + case nameSource = "name_source" case position = "position" case target = "target" } + /// The strategy of how the auto click action name is computed + public enum NameSource: String, Codable { + case customAttribute = "custom_attribute" + case maskPlaceholder = "mask_placeholder" + case standardAttribute = "standard_attribute" + case textContent = "text_content" + case maskDisallowed = "mask_disallowed" + case blank = "blank" + } + /// Action position properties public struct Position: Codable { /// X coordinate relative to the target element of the action (in pixels) @@ -358,6 +372,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 +437,7 @@ public struct RUMActionEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -478,7 +494,7 @@ public struct RUMErrorEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -493,7 +509,7 @@ public struct RUMErrorEvent: RUMDataModel { public var error: Error /// Feature flags properties - public internal(set) var featureFlags: FeatureFlags? + public var featureFlags: FeatureFlags? /// Properties of App Hang and ANR errors public let freeze: Freeze? @@ -517,7 +533,7 @@ public struct RUMErrorEvent: RUMDataModel { public let type: String = "error" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -650,6 +666,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 +820,8 @@ public struct RUMErrorEvent: RUMDataModel { case aNR = "ANR" case appHang = "App Hang" case exception = "Exception" + case watchdogTermination = "Watchdog Termination" + case memoryWarning = "Memory Warning" } /// Properties for one of the error causes @@ -1003,7 +1022,7 @@ public struct RUMErrorEvent: RUMDataModel { /// Feature flags properties public struct FeatureFlags: Codable { - public internal(set) var featureFlagsInfo: [String: Encodable] + public var featureFlagsInfo: [String: Encodable] } /// Properties of App Hang and ANR errors @@ -1043,6 +1062,7 @@ public struct RUMErrorEvent: RUMDataModel { case reactNative = "react-native" case roku = "roku" case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" } /// View properties @@ -1123,7 +1143,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let container: Container? /// User provided context - public internal(set) var context: RUMEventAttributes? + public var context: RUMEventAttributes? /// Start of the event in ms from epoch public let date: Int64 @@ -1156,7 +1176,7 @@ public struct RUMLongTaskEvent: RUMDataModel { public let type: String = "long_task" /// User properties - public internal(set) var usr: RUMUser? + public var usr: RUMUser? /// The version for this application public let version: String? @@ -1291,6 +1311,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 @@ -1330,19 +1351,112 @@ public struct RUMLongTaskEvent: RUMDataModel { /// Long Task properties public struct LongTask: Codable { - /// Duration in ns of the long task + /// Duration in ns for which the animation frame was being blocked + public let blockingDuration: Int64? + + /// Duration in ns of the long task or long animation frame public let duration: Int64 - /// UUID of the long task + /// Type of the event: long task or long animation frame + public let entryType: EntryType? + + /// Start time of of the first UI event (mouse/keyboard and so on) to be handled during the course of this frame + public let firstUiEventTimestamp: Double? + + /// UUID of the long task or long animation frame public let id: String? /// Whether this long task is considered a frozen frame public let isFrozenFrame: Bool? + /// Start time of the rendering cycle, which includes requestAnimationFrame callbacks, style and layout calculation, resize observer and intersection observer callbacks + public let renderStart: Double? + + /// A list of long scripts that were executed over the course of the long frame + public let scripts: [Scripts]? + + /// Start time of the long animation frame + public let startTime: Double? + + /// Start time of the time period spent in style and layout calculations + public let styleAndLayoutStart: Double? + enum CodingKeys: String, CodingKey { + case blockingDuration = "blocking_duration" case duration = "duration" + case entryType = "entry_type" + case firstUiEventTimestamp = "first_ui_event_timestamp" case id = "id" case isFrozenFrame = "is_frozen_frame" + case renderStart = "render_start" + case scripts = "scripts" + case startTime = "start_time" + case styleAndLayoutStart = "style_and_layout_start" + } + + /// Type of the event: long task or long animation frame + public enum EntryType: String, Codable { + case longTask = "long-task" + case longAnimationFrame = "long-animation-frame" + } + + public struct Scripts: Codable { + /// Duration in ns between startTime and when the subsequent microtask queue has finished processing + public let duration: Int64? + + /// Time after compilation + public let executionStart: Double? + + /// Duration in ns of the the total time spent processing forced layout and style inside this function + public let forcedStyleAndLayoutDuration: Int64? + + /// Information about the invoker of the script + public let invoker: String? + + /// Type of the invoker of the script + public let invokerType: InvokerType? + + /// Duration in ns of the total time spent in 'pausing' synchronous operations (alert, synchronous XHR) + public let pauseDuration: Int64? + + /// The script character position where available (or -1 if not found) + public let sourceCharPosition: Int64? + + /// The script function name where available (or empty if not found) + public let sourceFunctionName: String? + + /// The script resource name where available (or empty if not found) + public let sourceUrl: String? + + /// Time the entry function was invoked + public let startTime: Double? + + /// The container (the top-level document, or an