diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..4157440bf2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e7f66d918..97184c5c62 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,8 @@ # Global code owners - RUM Mobile Team -* @DataDog/rum-mobile +* @DataDog/rum-mobile @DataDog/rum-mobile-ios ## Docs -/docs/ @DataDog/documentation @DataDog/rum-mobile -*README.md @DataDog/documentation @DataDog/rum-mobile \ No newline at end of file +/docs/ @DataDog/documentation @DataDog/rum-mobile +*README.md @DataDog/documentation @DataDog/rum-mobile diff --git a/.github/ISSUE_TEMPLATE/BugReport.yml b/.github/ISSUE_TEMPLATE/BugReport.yml new file mode 100644 index 0000000000..c03c6f1a6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BugReport.yml @@ -0,0 +1,111 @@ +name: Bug Report +description: Is the SDK not working as expected? Help us improve by submitting a bug report. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Ensure you go through our [troubleshooting](https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/troubleshooting/#debugging-1) page before creating a new issue. + Before getting started, if the problem is urgent or easier to investigate with access to your organization's data please use our [official support channel](https://www.datadoghq.com/support/). + - type: textarea + id: description + attributes: + label: Describe the bug + description: Provide a clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: | + Provide a self-contained piece of code demonstrating the bug. + For a more complex setup consider creating a small app that showcases the problem. + **Note** - Avoid sharing any business logic, credentials or tokens. + validations: + required: true + - type: textarea + id: logs + attributes: + label: SDK logs + description: | + Please provide console logs before, during and after the bug occurs. + validations: + required: false + - type: textarea + id: expected_behavior + attributes: + label: Expected behavior + description: Provide a clear and concise description of what you expected the SDK to do. + validations: + required: false + - type: input + id: affected_sdk_versions + attributes: + label: Affected SDK versions + description: What are the SDK versions you're seeing this bug in? + validations: + required: true + - type: input + id: last_working_sdk_version + attributes: + label: Latest working SDK version + description: What was the last SDK version that was working as expected? + validations: + required: true + - type: dropdown + id: checked_lastest_sdk + attributes: + label: Did you confirm if the latest SDK version fixes the bug? + options: + - 'Yes' + - 'No' + validations: + required: true + - type: dropdown + id: integration_method + attributes: + label: Integration Methods + options: + - SPM + - Cocoapods + - Carthage + - XCFramework + - Source + validations: + required: true + - type: input + id: xcode_version + attributes: + label: Xcode Version + description: e.g. Xcode 11.5 (15C500b), obtained with **xcodebuild -version** + - type: input + id: swift_version + attributes: + label: Swift Version + description: e.g. Swift 5.9 , obtained with **swift —version** + - type: input + id: mac_version + attributes: + label: MacOS Version + description: e.g. macOS Catalina 10.15.5 (19F96), obtained with **sw_vers** + - type: input + id: deployment_targe + attributes: + label: Deployment Target + description: | + What is the Deployment Target of your app? e.g. *iOS 12*, *iPhone* + *iPad* + - type: textarea + id: device_info + attributes: + label: Device Information + description: | + What are the common characteristics of devices you're seeing this bug in. + Specific models, OS versions, network state (wifi / cellular / offline), power state (plugged in / battery), etc. + - type: textarea + id: other_info + attributes: + label: Other relevant information + description: | + Other relevant information such as additional tooling in place, proxies, etc. + Anything that might be relevant for troubleshooting this bug. diff --git a/.github/ISSUE_TEMPLATE/CrashReport.yml b/.github/ISSUE_TEMPLATE/CrashReport.yml new file mode 100644 index 0000000000..74204d28b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CrashReport.yml @@ -0,0 +1,76 @@ +name: Crash Report +description: Report crashes caused by the SDK. +labels: ["crash"] +body: + - type: markdown + attributes: + value: | + Report crashes caused by the SDK. Please try to be as detailed as possible. + Before getting started, if the problem is urgent please use our [official support channel](https://www.datadoghq.com/support/). + - type: textarea + id: stacktrace + attributes: + label: Stack trace + description: Please provide us with the stack trace of the crash or a crash report. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: | + Provide a self-contained piece of code demonstrating the crash if you can. + For a more complex setup consider creating a small app that showcases the problem. + **Note** - Avoid sharing any business logic, credentials or tokens. + validations: + required: false + - type: input + id: volume + attributes: + label: Volume + description: What percentage of your app sessions are impacted with this crash? + validations: + required: true + - type: input + id: affected_sdk_versions + attributes: + label: Affected SDK versions + description: What are the SDK versions you're seeing this crash in? + validations: + required: true + - type: input + id: last_working_sdk_version + attributes: + label: Latest working SDK version + description: If you know, what was the last SDK version where the crash did manifest itself? + validations: + required: true + - type: dropdown + id: checked_lastest_sdk + attributes: + label: Does the crash manifest in the latest SDK version? + options: + - 'Yes' + - 'No' + validations: + required: true + - type: input + id: deployment_targe + attributes: + label: Deployment Target + description: | + What is the Deployment Target of your app? e.g. *iOS 12*, *iPhone* + *iPad* + - type: textarea + id: device_info + attributes: + label: Device Information + description: | + What are the common characteristics of devices you're seeing this crash in? + Specific models, OS versions, etc. + validations: + required: false + - type: textarea + id: other_info + attributes: + label: Other relevant information + description: Anything that might be relevant to pinpoint the source of the crash. diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yml b/.github/ISSUE_TEMPLATE/FeatureRequest.yml new file mode 100644 index 0000000000..7e7324be4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yml @@ -0,0 +1,32 @@ +name: Feature Request +description: Have an idea or need a new feature? Request it here. +labels: ["feature"] +body: + - type: textarea + id: description + attributes: + label: Feature description + description: | + Provide a description for the feature request. Please include: + 1. Use case + 2. How the SDK currently delivers (or doesn't) + 3. What would you like to see + validations: + required: true + - type: textarea + id: proposed_solution + attributes: + label: Proposed solution + description: | + How would you implement this? + Propose an idea, solution or reference implementation. + validations: + required: false + - type: textarea + id: other_info + attributes: + label: Other relevant information + description: Any other relevant information you'd like we take into consideration. + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/Question.yml b/.github/ISSUE_TEMPLATE/Question.yml new file mode 100644 index 0000000000..0f286a3568 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.yml @@ -0,0 +1,10 @@ +name: Question +description: Do you just have a question about the SDK or a product? Ask here. +labels: ["question"] +body: + - type: textarea + id: question + attributes: + label: Question + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/SetupIssue.yml b/.github/ISSUE_TEMPLATE/SetupIssue.yml new file mode 100644 index 0000000000..4812eeb0a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/SetupIssue.yml @@ -0,0 +1,81 @@ +name: Setup Issue +description: Having a hard time setting up the SDK for the first time? Maybe a compilation issue or just nothing seems to be happening. Seek help with this. +labels: ["compilation issue"] +body: + - type: markdown + attributes: + value: | + Before creating an issue, please ensure you go through the [troubleshooting page](https://docs.datadoghq.com/real_user_monitoring/mobile_and_tv_monitoring/troubleshooting/#debugging-1). + - type: textarea + id: issue + attributes: + label: Describe the issue + description: Provide a clear and concise description of the issue. Include compilation logs and SDK debug logs if relevant. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction steps + description: | + Provide a self-contained piece of code demonstrating the issue. + For a more complex setup consider creating a small app that showcases the problem. + **Note** - Avoid sharing any business logic, credentials or tokens. + validations: + required: true + - type: textarea + id: device_info + attributes: + label: Device Information + description: | + What are the common characteristics of devices you're seeing this issue in? + Simulators, specific models, OS versions, network state (wifi / cellular / offline), power state (plugged in / battery), etc. + validations: + required: false + - type: input + id: sdk_version + attributes: + label: SDK version + description: Which SDK version are you trying to use? + validations: + required: true + - type: dropdown + id: integration_method + attributes: + label: Integration Methods + options: + - SPM + - Cocoapods + - Carthage + - XCFramework + - Source + validations: + required: true + - type: input + id: xcode_version + attributes: + label: Xcode Version + description: e.g. Xcode 11.5 (15C500b), obtained with **xcodebuild -version** + - type: input + id: swift_version + attributes: + label: Swift Version + description: e.g. Swift 5.9 , obtained with **swift —version** + - type: input + id: mac_version + attributes: + label: MacOS Version + description: e.g. macOS Catalina 10.15.5 (19F96), obtained with **sw_vers** + - type: input + id: deployment_targe + attributes: + label: Deployment Target + description: | + What is the Deployment Target of your app? e.g. *iOS 12*, *iPhone* + *iPad* + - type: textarea + id: other_info + attributes: + label: Other relevant information + description: | + Other relevant information such as additional tooling in place, proxies, etc. + Anything that might be relevant for troubleshooting your setup. diff --git a/.github/ISSUE_TEMPLATE/compilation_issue.md b/.github/ISSUE_TEMPLATE/compilation_issue.md deleted file mode 100644 index 840e566a2b..0000000000 --- a/.github/ISSUE_TEMPLATE/compilation_issue.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: Compilation Issue -about: Having a Cocoapods / Carthage / SPM problem when linking the SDK? -title: '' -labels: compilation issue -assignees: '' - ---- - -### The issue - -📝 Give us the error message you receive, describe the problem and answer the questions. - ---- - -#### Datadog SDK version: - -_Which version of the Datadog SDK causes this problem? e.g. `1.2.0`_ - -#### Last working Datadog SDK version: - -_What is the last Datadog SDK version where this problem didn't occur? e.g. `1.1.0`_ - -#### Dependency Manager: - -_Which dependency manager do you use? e.g. Cocoapods / Carthage / SPM / ..._ - -#### Other toolset: - -_Do you use additional tools with your dependency manager? e.g. [CarthageCache](https://github.com/Wolox/carthage_cache)_ - -#### Xcode version: - -_e.g. `Xcode 11.5 (11E608c)`_ - -#### Swift version: - -_e.g. `5.1`_ - -#### Deployment Target: - -_What is the Deployment Target of your app? e.g. `iOS 12`, `iPhone` + `iPad`_ - -#### macOS version: - -_e.g. `macOS Catalina 10.15.5 (19F96)`_ diff --git a/.github/ISSUE_TEMPLATE/crash_report.md b/.github/ISSUE_TEMPLATE/crash_report.md deleted file mode 100644 index f32d11f9a0..0000000000 --- a/.github/ISSUE_TEMPLATE/crash_report.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Crash -about: Noticed the SDK crash? -title: '' -labels: crash -assignees: '' - ---- - -### The crash - -📝 Give us the crash report or stack trace, describe the problem in details and answer the questions. - ---- - -#### Datadog SDK versions: - -_Which version(s) of the Datadog SDK you see this crash happening in?_ - -#### Last stable Datadog SDK version: - -_What is the last Datadog SDK version where this crash doesn't happen?_ - -#### Volume: - -_What % of your app sessions is impacted with this crash?_ - -#### OS version: - -_Which iOS versions does this crash happen on?_ - -#### Deployment Target: - -_What is the Deployment Target of your app? e.g. `iOS 12`, `iPhone` + `iPad`_ - -#### Device version: - -_Which devices does this crash happen on? e.g. `iPhone X` only or various iPads_ - -#### Environment: - -_Do you notice any environment correlation in crash reports? e.g. low battery, no internet connection, memory pressure_ diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md deleted file mode 100644 index 951afbf02c..0000000000 --- a/.github/ISSUE_TEMPLATE/other.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Other -about: Noticed a bug, having a question or a feature request? -title: '' -assignees: '' - ---- - -### The thing - -Tell us the thing 🙂 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 046e8937a3..2f187a2865 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ A short description of what changes this PR introduces and why. A brief description of implementation details of this PR. ### Review checklist - - [ ] Feature or bugfix MUST have appropriate tests (unit, integration) -- [ ] Make sure each commit and the PR mention the Issue number or JIRA reference +- [ ] Make sure each commit and the PR mention the Issue number or JIRA reference +- [ ] Add CHANGELOG entry for user facing changes +- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) [internal]) and run `make api-surface`) diff --git a/.gitignore b/.gitignore index 0ecf54e6db..896abc144a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,25 @@ .DS_Store /build -/.build -/.swiftpm -xcuserdata/ +.build +.swiftpm +Package.resolved +Carthage/Build +Carthage/Checkouts +xcuserdata/ *.local.xcconfig +E2ETests/code-signing +tools/dogfooding/repos + +# Ignore files for Python tools: +.idea +venv +*.pyc +__pycache__ +*.swp +.venv +.vscode +*.pytest_cache + +# CI job artifacts +artifacts/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..17427b4de0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,391 @@ +stages: + - pre + - lint + - test + - ui-test + - smoke-test + - e2e-test + - benchmark-test + - dogfood + - release-build + - release-publish + - post + +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: + - 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 + rules: + - !reference [.test-pipeline-job, rules] + script: + - make clean repo-setup ENV=ci + - make lint license-check + - make rum-models-verify sr-models-verify + +Unit Tests (iOS): + stage: 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" + - 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 + 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" + - 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: + - macos:ventura + - specific:true + variables: + 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/.spi.yml b/.spi.yml new file mode 100644 index 0000000000..5d317a7c02 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - platform: ios + documentation_targets: ["DatadogInternal", "DatadogCore", "DatadogObjc", "DatadogLogs", "DatadogTrace", "DatadogRUM", "DatadogSessionReplay", "DatadogCrashReporting", "DatadogWebViewTracking"] 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/spm/SPMProject.xcodeproj.src/project.xcworkspace/contents.xcworkspacedata b/BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.xcworkspace/contents.xcworkspacedata rename to BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Shopist/Shopist.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Shopist/Shopist.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to BenchmarkTests/BenchmarkTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..73079a7d00 --- /dev/null +++ b/BenchmarkTests/BenchmarkTests.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Shopist/Shopist/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 similarity index 100% rename from Shopist/Shopist/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to BenchmarkTests/UIKitCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json 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/BenchmarkTests/UIKitCatalog/ModuleBundle.swift b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift new file mode 100644 index 0000000000..9821bc2283 --- /dev/null +++ b/BenchmarkTests/UIKitCatalog/ModuleBundle.swift @@ -0,0 +1,15 @@ +/* + * 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 + +private class ModuleClass { } + +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 new file mode 100644 index 0000000000..43672d77f8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,846 @@ +# 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][] +- [IMPROVEMENT] Add `.connect`, `.trace`, `.options` values to `DDRUMMethod` type. See [#1886][] +- [FIX] Fix compilation issues on Xcode 16 beta. See [#1898][] + +# 2.12.0 / 03-06-2024 + +- [IMPROVEMENT] Crash errors now include up-to-date global RUM attributes. See [#1834][] +- [FEATURE] `DatadogTrace` now supports OpenTelemetry. See [#1828][] +- [FIX] Fix crash on accessing request.allHTTPHeaderFields. See [#1843][] +- [FEATURE] Support for trace context injection configuration to allow selective injection. See [#1835][] +- [FEATURE] `DatadogWebViewTracking` is now available for Obj-C. See [#1854][] +- [FEATURE] RUM "stop session", "get session ID" and "evaluate feature flag" APIs are now available for Obj-C. See [#1853][] + +# 2.11.0 / 08-05-2024 + +- [FEATURE] `DatadogTrace` now supports head-based sampling. See [#1794][] +- [FEATURE] Support WebView recording in Session Replay. See [#1776][] +- [IMPROVEMENT] Add `isInitialized` and `stopInstance` methods to ObjC API. See [#1800][] +- [IMPROVEMENT] Add `addUserExtraInfo` method to ObjC API. See [#1799][] +- [FIX] Add background upload capability to extensions. See [#1803][] +- [IMPROVEMENT] Start sending data immediately after SDK is initialized. See [#1798][] +- [IMPROVEMENT] Make the SDK compile on macOS 12+. See [#1711][] + +# 2.10.1 / 02-05-2024 + +- [FIX] Use trace and span id as decimal. See [#1807][] + +# 2.10.0 / 23-04-2024 + +- [IMPROVEMENT] Add image duplicate detection between sessions. See [#1747][] +- [FEATURE] Add support for 128 bit trace IDs. See [#1721][] +- [FEATURE] Fatal App Hangs are tracked in RUM. See [#1763][] +- [FIX] Avoid name collision with Required Reason APIs. See [#1774][] + +# 2.9.0 / 11-04-2024 + +- [FEATURE] Call RUM's `errorEventMapper` for crashes. See [#1742][] +- [FEATURE] Support calling log event mapper for crashes. See [#1741][] +- [FIX] Fix crash in `NetworkInstrumentationFeature`. See [#1767][] +- [FIX] Remove modulemap. See [#1746][] +- [FIX] Expose objc interfaces in Session Replay module. See [#1697][] + +# 2.8.1 / 20-03-2024 + +- [FEATURE] App Hangs are tracked as RUM errors. See [#1685][] +- [FIX] Propagate parent span in distributing tracing. See [#1627][] +- [IMPROVEMENT] Add Device's Brand, Name, and Model in LogEvent. See [#1672][] (Thanks [@aldoKelvianto][]) +- [FEATURE] Improved image recording in Session Replay. See [#1592][] +- [FEATURE] Allow custom error fingerprinting on logs with a special attribute. See [#1722][] +- [FEATURE] Add global log attributes. See [#1707][] +- [FEATURE] Privacy Manifest data usage description. See [#1724][] +- [FIX] Pass through data when network request completes. See [#1696][] + +# 2.7.1 / 12-02-2024 + +- [FIX] Privacy Report missing properties. See [#1656][] +- [FIX] Privacy manifest collision in static framework. See [#1666][] + +# 2.7.0 / 25-01-2024 + +- [FIX] RUM session not being linked to spans. See [#1615][] +- [FIX] `URLSessionTask.resume()` swizzling in iOS 13 and 12. See [#1637][] +- [FEATURE] Allow stopping a core instance. See [#1541][] +- [FEATURE] Link crashes sent as Log events to RUM session. See [#1645][] +- [IMPROVEMENT] Add extra HTTP codes to the list of retryable status codes. See [#1639][] +- [FEATURE] Add privacy manifest to `DatadogCore`. See [#1644][] + +# 2.6.0 / 09-01-2024 +- [FEATURE] Add `currentSessionID(completion:)` accessor to access the current session ID. +- [FEATURE] Add `BatchProcessingLevel` configuration allowing to process more batches within single read/upload cycle. See [#1531][] +- [FIX] Use `currentRequest` instead `originalRequest` for URLSession request interception. See [#1609][] +- [FIX] Remove weak `UIViewController` references. See [#1597][] + +# 2.5.1 / 20-12-2023 + +- [BUGFIX] Fix `view.time_spent` in RUM view events. See [#1596][] + +- [FEATURE] Start RUM session on RUM init. See [#1594][] + +# 2.5.0 / 08-11-2023 + +- [BUGFIX] Optimize Session Replay diffing algorithm. See [#1524][] +- [FEATURE] Add network instrumentation for async/await URLSession APIs. See [#1394][] +- [FEATURE] Change default tracing headers for first party hosts to use both Datadog headers and W3C `tracecontext` headers. See [#1529][] +- [FEATURE] Add tracestate headers when using W3C tracecontext. See [#1536][] +- [BUGFIX] Fix RUM ViewController leaks. See [#1533][] + +# 2.4.0 / 18-10-2023 + +- [FEATURE] WebView Log events can be now sampled. See [#1515][] +- [BUGFIX] WebView RUM events are now dropped if mobile RUM session is not sampled. See [#1502][] +- [BUGFIX] Fix `os.name` in Log events. See [#1493][] + +# 2.3.0 / 02-10-2023 + +- [IMPROVEMENT] Add UIBackgroundTask for uploading jobs. See [#1412][] +- [IMPROVEMENT] Report Build Number in Logs and RUM. See [#1465][] +- [BUGFIX] Fix wrong `view.name` reported in RUM crashes. See [#1488][] +- [BUGFIX] Fix RUM sessions state propagation in Crash Reporting. See [#1498][] + +# 2.2.1 / 13-09-2023 + +- [BUGFIX] Add default RUM views and actions predicates to DatadogObjc . See [#1464][]. + +# 2.2.0 / 12-09-2023 + +- [IMPROVEMENT] Enable cross-platform SDKs to change app `version`. See [#1447][] +- [IMPROVEMENT] Enable cross-platform SDKs to edit more of telemetry configuration. See [#1456][] + +# 2.1.2 / 29-08-2023 + +- [BUGFIX] Do not embed DatadogInternal while building Trace and RUM xcframeworks. See [#1444][]. + +# 2.1.1 / 22-08-2023 + +- [BUGFIX] `DatadogObjc` not fully available in `2.1.0`. See [#1428][]. + +# 2.1.0 / 18-08-2023 + +- [BUGFIX] Manual trace injection APIs are not available in DatadogTrace. See [#1415][]. +- [BUGFIX] Fix session replay uploads to AP1 site. See [#1418][]. +- [BUGFIX] Allow instantiating custom instance of the SDK after default one. See [#1413][]. +- [BUGFIX] Do not propagate attributes from Errors and LongTasks to Views. +- [IMPROVEMENT] Upgrade to PLCrashReporter 1.11.1. +- [FEATURE] Report session sample rate to the backend with RUM events. See [#1410][] +- [IMPROVEMENT] Expose Session Replay to Objective-C. see [#1419][] + +# 2.0.0 / 31-07-2023 + +Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATION.md) to upgrade from `1.x` versions. + +- [FEATURE] Session Replay. +- [FEATURE] Support multiple SDK instances. +- [IMPROVEMENT] All relevant products (RUM, Trace, Logs, etc.) are now extracted into different modules. +- [BUGFIX] Module stability: fix name collision. + +# 1.22.0 / 21-07-2023 +- [BUGFIX] Fix APM local spans not correlating with RUM views. See [#1355][] +- [IMPROVEMENT] Reduce number of view updates by filtering events from payload. See [#1328][] + +# 1.21.0 / 27-06-2023 +- [BUGFIX] Fix TracingUUID string format. See [#1311][] (Thanks [@changm4n][]) +- [BUGFIX] Rename _Datadog_Private to DatadogPrivate. See [#1331] (Thanks [@alexfanatics][]) +- [IMPROVEMENT] Add context to crash when there's an active view. See [#1315][] + + +# 1.20.0 / 01-06-2023 +- [BUGFIX] Use targetTimestamp as reference to calculate FPS for variable refresh rate displays. See [#1272][] + +# 1.19.0 / 26-04-2023 +- [BUGFIX] Fix view attributes override by action attributes. See [#1250][] +- [IMPROVEMENT] Add Tracer sampling rate. See [#1259][] +- [BUGFIX] Fix RUM context not being attached to log when no user action exists. See [#1264][] + +# 1.18.0 / 19-04-2023 +- [IMPROVEMENT] Add start reason to the session. See [#1247][] +- [IMPROVEMENT] Add ability to stop the session. See [#1219][] + +# 1.17.0 / 23-03-2023 +- [BUGFIX] Fix crash in `VitalInfoSampler`. See [#1216][] (Thanks [@cltnschlosser][]) +- [IMPROVEMENT] Fix Xcode analysis warning. See [#1220][] +- [BUGFIX] Send crashes to both RUM and Logs. See [#1209][] + +# 1.16.0 / 02-03-2023 +- [IMPROVEMENT] Always create an ApplicationLaunch view on session initialization. See [#1160][] +- [BUGFIX] Remove the data race caused by sampling on the RUM thread. See [#1177][] (Thanks [@cltnschlosser][]) +- [BUGFIX] Add ability to adjust configuration telemetry sampling rate. See [#1188][] + +# 1.15.0 / 23-01-2023 + +- [BUGFIX] Fix 'Could not allocate memory' after corrupted TLV. See [#1089][] (Thanks [@cltnschlosser][]) +- [BUGFIX] Fix error count on the view update event following a crash. See [#1145][] + +# 1.14.0 / 20-12-2022 + +- [IMPROVEMENT] Add a method for sending error attributes on logs as strings. See [#1051][]. +- [IMPROVEMENT] Add manual Open Telemetry b3 headers injection. See [#1057][] +- [IMPROVEMENT] Add automatic Open Telemetry b3 headers injection. See [#1061][] +- [IMPROVEMENT] Add manual and automatic W3C traceparent header injection. See [#1071][] + +# 1.13.0 / 08-11-2022 + +- [IMPROVEMENT] Improve console logs when using `DDNoopRUMMonitor`. See [#1007][] (Thanks [@dfed][]) +- [IMPROVEMENT] Add public API to control tracking of frustrations signals. See [#1013][] +- [IMPROVEMENT] Send trace sample rate (`dd.rulePsr`) for APM's traffic ingestion control page. See [#1029][] +- [IMPROVEMENT] Add a method to add user info properties. See [#1031][] +- [BUGFIX] Fix vitals default presets. See [#1043][] +- [IMPROVEMENT] Add logging sampling. See [#1045][] + + +# 1.12.1 / 18-10-2022 + +- [IMPROVEMENT] Upgrade to PLCrashReporter 1.11.0 to fix Xcode 14 support. + +# 1.12.0 / 16-09-2022 + +- [BUGFIX] Fix manual User Action dropped if a new view start. See [#997][] +- [IMPROVEMENT] Enable cross-platform SDKs to change app `version`. See [#973][] +- [IMPROVEMENT] Add internal APIs for cross-platform SDKs. See [#964][] +- [IMPROVEMENT] Add mobile vitals frequency configuration. See [#876][] +- [IMPROVEMENT] Include the exact model information in RUM `device.model`. See [#888][] +- [FEATURE] Allow filtering outgoing logs with a status threshold. See [#867][] +- [BUGFIX] Fix compilation issue in SwiftUI Previews. See [#949][] +- [IMPROVEMENT] Expose server date provider for custom clock synchronization. See [#950][] + +# 1.11.1 / 20-06-2022 + +### Changes + +- [BUGFIX] Fix Mac Catalyst builds compatibility. See [#894][] + +# 1.11.0 / 13-06-2022 + +### Changes + +- [BUGFIX] Fix rare problem with bringing up the "Local Network Permission" alert. See [#830][] +- [BUGFIX] Fix RUM event `source`. See [#832][] +- [BUGFIX] Stop reporting pre-warmed application launch time. See [#789][] +- [BUGFIX] Allow log event dropping. See [#795][] +- [FEATURE] Integration with CI Visibility Tests. See[#761][] +- [FEATURE] Add tvOS Support. See [#793][] +- [FEATURE] Add data encryption interface on-disk data storage. See [#797][] +- [IMPROVEMENT] Allow manually tracked resources in RUM Sessions to detect first party hosts. See [#837][] +- [IMPROVEMENT] Add tracing sampling rate. See [#851][] +- [IMPROVEMENT] Crash Reporting: Filter out unrecognized trailing `???` stack frame in `error.stack`. See [#794][] +- [IMPROVEMENT] Reduce the number of intermediate view events sent in RUM payloads. See [#815][] +- [IMPROVEMENT] Allow manually tracked resources in RUM Sessions to detect first party hosts. See [#837][] +- [IMPROVEMENT] Add tracing sampling rate. See [#851][] +- [BUGFIX] Fix rare problem with bringing up the "Local Network Permission" alert. See [#830][] +- [BUGFIX] Fix RUM event `source`. See [#832][] +- [FEATURE] Integration with CI Visibility Tests. See[#761][] +- [FEATURE] Add tvOS Support. See [#793][] +- [FEATURE] Add data encryption interface on-disk data storage. See [#797][] +- [BUGFIX] Stop reporting pre-warmed application launch time. See [#789][] +- [BUGFIX] Allow log event dropping. See [#795][] +- [IMPROVEMENT] Crash Reporting: Filter out unrecognized trailing `???` stack frame in `error.stack`. See [#794][] +- [IMPROVEMENT] Reduce the number of intermediate view events sent in RUM payloads. See [#815][] + +# 1.10.0 / 04-12-2022 + +### Changes + +- [FEATURE] Web-view tracking. See [#729][] +- [BUGFIX] Strip query parameters from span resource. See [#728][] + +# 1.9.0 / 01-26-2022 + +### Changes + +- [BUGFIX] Report binary image with no UUID. See [#724][] +- [FEATURE] Add Application Launch events tracking. See [#699][] +- [FEATURE] Set `PLCrashReporter` custom path. See [#692][] +- [FEATURE] `SwiftUI` Instrumentation. See [#676][] +- [IMPROVEMENT] Embed Kronos. See [#708][] +- [IMPROVEMENT] Add `@service` attribute to all RUM events. See [#725][] +- [IMPROVEMENT] Adds support for flutter error source. See [#715][] +- [IMPROVEMENT] Add crash reporting console logs. See [#712][] +- [IMPROVEMENT] Keep view active until all resources are consumed. See [#702][] +- [IMPROVEMENT] Allow passing in a type for errors sent with a message. See [#680][] (Thanks [@AvdLee][]) +- [IMPROVEMENT] Add config overrides for debug launch arguments. See [#679][] + +# 1.8.0 / 11-23-2021 + +### Changes + +- [BUGFIX] Fix rare crash in `CarrierInfoProvider`. See [#627][] [#623][], [#619][] (Thanks [@safa-ads][], [@matcartmill][]) +- [BUGFIX] Crash Reporting: Fix issue with some truncated stack traces not being displayed. See [#641][] +- [BUGFIX] Fix reading SDK attributes in Objective-C. See [#654][] +- [FEATURE] RUM: Track slow UI renders with RUM Long Tasks. See [#567][] +- [FEATURE] RUM: Add API to notify RUM session start: `.onRUMSessionStart(_: (String, Bool) -> Void)`. See [#590][] +- [FEATURE] Logs: Add logs scrubbing API: `.setLogEventMapper(_: (LogEvent) -> LogEvent)`. See [#640][] +- [FEATURE] Add `Datadog.isInitialized` API. See [#566][] +- [FEATURE] Add API for clearing out all SDK data: `Datadog.clearAllData()`. See [#644][] +- [FEATURE] Add support for `us5` site. See [#576][] +- [FEATURE] Support `URLSession` proxy configuration with `.connectionProxyDictionary`. See [#582][] +- [IMPROVEMENT] Compress HTTP body in SDK uploads. See [#626][] +- [IMPROVEMENT] Change type of `.xhr` RUM Resources to `.native`. See [#605][] +- [IMPROVEMENT] Link logs and traces to RUM Actions. See [#615][] +- [IMPROVEMENT] Crash Reporting: Fix symbolication issue for iOS Simulator crashes. See [#563][] +- [IMPROVEMENT] Fix various typos in docs. See [#569][] (Thanks [@michalsrutek][]) +- [IMPROVEMENT] Use Intake API V2 for SDK data uploads. See [#562][] + +# 1.7.2 / 11-8-2021 + +### Changes + +- [BUGFIX] Fix iOS 15 crash related to `ProcessInfo.isLowPowerModeEnabled`. See [#609][] [#655][] (Thanks [@pingd][]) + +# 1.7.1 / 10-4-2021 + +### Changes + +- [BUGFIX] Fix iOS 15 crash in `MobileDevice.swift`. See [#609][] [#613][] (Thanks [@arnauddorgans][], [@earltedly][]) +- [BUGFIX] RUM: Fix bug with "Refresh Rate" Mobile Vital reporting very low values. [#608][] + +# 1.7.0 / 09-27-2021 + +### Changes + +- [BUGFIX] RUM: Fix `DDRUMView` API visibility for Objective-C. See [#583][] +- [FEATURE] Crash Reporting: Add `DatadogCrashReporting` +- [FEATURE] RUM: Add Mobile Vitals. See [#493][] [#514][] [#522][] [#495][] +- [FEATURE] RUM: Add option for renaming instrumented actions. See [#539][] +- [FEATURE] RUM: Add option for tracking events when app is in background. See [#504][] [#537][] +- [FEATURE] Add support for `us3` site. See [#523][] +- [IMPROVEMENT] RUM: Improve RUM <> APM integration. See [#524][] [#575][] [#531][] (Thanks [@jracollins][], [@marcusway][]) +- [IMPROVEMENT] RUM: Improve naming for views started with `key:`. See [#534][] +- [IMPROVEMENT] RUM: Improve actions instrumentation. See [#509][] [#545][] [#547][] +- [IMPROVEMENT] RUM: Sanitize custom timings for views. See [#525][] +- [IMPROVEMENT] Do not retry uploading events if Client Token is invalid. See [#535][] + +# 1.6.0 / 06-09-2021 + +### Changes + +- [BUGFIX] Trace: Fix `[configuration trackUIKitRUMViews]` not working properly in Obj-c. See [#419][] +- [BUGFIX] Trace: Make `tracePropagationHTTPHeaders` available in Obj-c. See [#421][] (Thanks [@ben-yolabs][]) +- [BUGFIX] RUM: Fix RUM Views auto-instrumentation issue on iOS 11. See [#474][] +- [FEATURE] RUM: Support adding custom attributes for auto-instrumented RUM Resources. See [#473][] +- [FEATURE] Trace: Add scrubbing APIs for redacting auto-instrumented spans. See [#481][] +- [IMPROVEMENT] RUM: Add "VIEW NAME" attribute to RUM Views. See [#318][] +- [IMPROVEMENT] RUM: Views cannot be now dropped using view event mapper. See [#415][] +- [IMPROVEMENT] RUM: Improve presentation of errors sent with `Logger`. See [#423][] +- [IMPROVEMENT] Trace: Improve presentation of errors sent with `span.log()`. See [#431][] +- [IMPROVEMENT] Add support for extra user attributes in Obj-c. See [#444][] +- [IMPROVEMENT] Trace: Add `foreground_duration` and `is_background` information to network spans. See [#436][] +- [IMPROVEMENT] RUM: Views will now automatically stop when the app leaves foreground. See [#479][] +- [IMPROVEMENT] `DDURLSessionDelegate` can now be initialized before starting SDK. See [#483][] + +# 1.5.2 / 04-13-2021 + +### Changes + +- [BUGFIX] Add missing RUM Resource APIs to RUM for Objc. See [#447][] (Thanks [@sdejesusF][]) +- [BUGFIX] Fix eventual `swiftlint` error during `carthage` builds. See [#450][] +- [IMPROVEMENT] Improve cocoapods installation by not requiring `!use_frameworks`. See [#451][] + +# 1.5.1 / 03-11-2021 + +### Changes + +- [BUGFIX] Carthage XCFrameworks support. See [#439][] + +# 1.5.0 / 03-04-2021 + +### Changes + +- [BUGFIX] Fix baggage items propagation issue for `Span`. See [#365][] (Thanks [@philtre][]) +- [FEATURE] Add set of scrubbing APIs for redacting and dropping particular RUM Events. See [#367][] +- [FEATURE] Add support for GDPR compliance with new `Datadog.set(trackingConsent:)` API. See [#335][] +- [FEATURE] Add `Global.rum.addTiming(name:)` API for marking custom tming events in RUM Views. See [#323][] +- [FEATURE] Add support for Alamofire networking with `DatadogAlamofireExtension`. See [#340][] +- [FEATURE] Add configuration of data upload frequency and paylaod size with `.set(batchSize:)` and `.set(uploadFrequency:)` APIs. See [#358][] +- [FEATURE] Add convenient `.setError(_:)` API for setting `Error` on `Span`. See [#390][] +- [IMPROVEMENT] Improve `DATE` accurracy (with NTP time sync) for all data send from the SDK. See [#327][] +- [IMPROVEMENT] Improve App Launch Time metric accurracy. See [#381][] + +# 1.4.1 / 01-18-2021 + +### Changes + +- [BUGFIX] Fix app extension compilation issue for `UIApplication.shared` symbol. See [#370][] (Thanks [@SimpleApp][]) + +# 1.4.0 / 12-14-2020 + +### Changes + +- [BUGFIX] Fix crash when `serviceName` contains space characters. See [#317][] (Thanks [@philtre][]) +- [BUGFIX] Fix issue with data uploads when battery status is `.unknown`. See [#320][] +- [BUGFIX] Fix compilation issue for Mac Catalyst. See [#277][] (Thanks [@Hengyu][]) +- [FEATURE] RUM: Add RUM monitoring feature (manual and auto instrumentation) +- [FEATURE] Add single `.set(endpoint:)` API to configure all Datadog endpoints. See [#322][] +- [FEATURE] Add support for GovCloud endpoints. See [#235][] +- [FEATURE] Add support for extra user attributes. See [#315][] +- [FEATURE] Logs: Add `error: Error` attribute to logging APIs. See [#303][] (Thanks [@sdejesusF][]) +- [FEATURE] Trace: Add `span.setActive()` API for indirect referencing Spans. See [#187][] +- [FEATURE] Trace: Add `Global.sharedTracer.startRootSpan(...)` API. See [#236][] +- [IMPROVEMENT] Trace: Add auto instrumentation for `URLSessionTasks` created with no completion handler. See [#262][] +- [IMPROVEMENT] Extend allowed characters set for the `environment` value. See [#246][] (Thanks [@sdejesusF][]) +- [IMPROVEMENT] Improve data upload performance. See [#249][] + +# 1.3.1 / 08-14-2020 + +### Changes + +- [BUGFIX] Fix SPM compilation issue for DatadogObjC. See [#220][] (Thanks [@TsvetelinVladimirov][]) +- [BUGFIX] Fix compilation issue in Xcode 11.3.1. See [#217][] (Thanks [@provTheodoreNewell][]) + +# 1.3.0 / 08-03-2020 + +### Changes + +- [FEATURE] Trace: Add tracing feature following the Open Tracing spec + +# 1.2.4 / 07-17-2020 + +### Changes + +- [BUGFIX] Logs: Fix out-of-memory crash on intensive logging. See [#185][] (Thanks [@hyling][]) + +# 1.2.3 / 07-15-2020 + +### Changes + +- [BUGFIX] Logs: Fix memory leaks in logs upload. See [#180][] (Thanks [@hyling][]) +- [BUGFIX] Fix App Store Connect validation issue for `DatadogObjC`. See [#182][] (Thanks [@hyling][]) + +# 1.2.2 / 06-12-2020 + +### Changes + +- [BUGFIX] Logs: Fix occasional logs malformation. See [#133][] + +# 1.2.1 / 06-09-2020 + +### Changes + +- [BUGFIX] Fix `ISO8601DateFormatter` crash on iOS 11.0 and 11.1. See [#129][] (Thanks [@lgaches][], [@Britton-Earnin][]) + +# 1.2.0 / 05-22-2020 + +### Changes + +- [BUGFIX] Logs: Fixed family of `NWPathMonitor` crashes. See [#110][] (Thanks [@LeffelMania][], [@00FA9A][], [@jegnux][]) +- [FEATURE] Logs: Change default `serviceName` to app bundle identifier. See [#102][] +- [IMPROVEMENT] Logs: Add milliseconds precision. See [#96][] (Thanks [@flobories][]) +- [IMPROVEMENT] Logs: Deliver logs faster in app extensions. See [#84][] (Thanks [@lmramirez][]) +- [OTHER] Logs: Change default `source` to `"ios"`. See [#111][] +- [OTHER] Link SDK as dynamic framework in SPM. See [#82][] + +# 1.1.0 / 04-21-2020 + +### Changes + +- [BUGFIX] Fix "Missing required module 'Datadog_Private'" Carthage error. See [#80][] +- [IMPROVEMENT] Logs: Sync logs time with server. See [#65][] + +# 1.0.2 / 04-08-2020 + +### Changes + +- [BUGFIX] Fix "'module.modulemap' should be inside the 'include' directory" Carthage error. See [#73][] (Thanks [@joeydong][]) + +# 1.0.1 / 04-07-2020 + +### Changes + +- [BUGFIX] Fix "out of memory" crash. See [#64][] (Thanks [@lmramirez][]) + +# 1.0.0 / 03-31-2020 + +### Changes + +- [FEATURE] Logs: Add logging feature + + + +[#64]: https://github.com/DataDog/dd-sdk-ios/issues/64 +[#65]: https://github.com/DataDog/dd-sdk-ios/issues/65 +[#73]: https://github.com/DataDog/dd-sdk-ios/issues/73 +[#80]: https://github.com/DataDog/dd-sdk-ios/issues/80 +[#82]: https://github.com/DataDog/dd-sdk-ios/issues/82 +[#84]: https://github.com/DataDog/dd-sdk-ios/issues/84 +[#96]: https://github.com/DataDog/dd-sdk-ios/issues/96 +[#102]: https://github.com/DataDog/dd-sdk-ios/issues/102 +[#110]: https://github.com/DataDog/dd-sdk-ios/issues/110 +[#111]: https://github.com/DataDog/dd-sdk-ios/issues/111 +[#129]: https://github.com/DataDog/dd-sdk-ios/issues/129 +[#133]: https://github.com/DataDog/dd-sdk-ios/issues/133 +[#180]: https://github.com/DataDog/dd-sdk-ios/issues/180 +[#182]: https://github.com/DataDog/dd-sdk-ios/issues/182 +[#185]: https://github.com/DataDog/dd-sdk-ios/issues/185 +[#187]: https://github.com/DataDog/dd-sdk-ios/issues/187 +[#217]: https://github.com/DataDog/dd-sdk-ios/issues/217 +[#220]: https://github.com/DataDog/dd-sdk-ios/issues/220 +[#235]: https://github.com/DataDog/dd-sdk-ios/issues/235 +[#236]: https://github.com/DataDog/dd-sdk-ios/issues/236 +[#246]: https://github.com/DataDog/dd-sdk-ios/issues/246 +[#249]: https://github.com/DataDog/dd-sdk-ios/issues/249 +[#262]: https://github.com/DataDog/dd-sdk-ios/issues/262 +[#277]: https://github.com/DataDog/dd-sdk-ios/issues/277 +[#303]: https://github.com/DataDog/dd-sdk-ios/issues/303 +[#315]: https://github.com/DataDog/dd-sdk-ios/issues/315 +[#317]: https://github.com/DataDog/dd-sdk-ios/issues/317 +[#318]: https://github.com/DataDog/dd-sdk-ios/issues/318 +[#320]: https://github.com/DataDog/dd-sdk-ios/issues/320 +[#322]: https://github.com/DataDog/dd-sdk-ios/issues/322 +[#323]: https://github.com/DataDog/dd-sdk-ios/issues/323 +[#327]: https://github.com/DataDog/dd-sdk-ios/issues/327 +[#335]: https://github.com/DataDog/dd-sdk-ios/issues/335 +[#340]: https://github.com/DataDog/dd-sdk-ios/issues/340 +[#358]: https://github.com/DataDog/dd-sdk-ios/issues/358 +[#365]: https://github.com/DataDog/dd-sdk-ios/issues/365 +[#367]: https://github.com/DataDog/dd-sdk-ios/issues/367 +[#370]: https://github.com/DataDog/dd-sdk-ios/issues/370 +[#381]: https://github.com/DataDog/dd-sdk-ios/issues/381 +[#390]: https://github.com/DataDog/dd-sdk-ios/issues/390 +[#415]: https://github.com/DataDog/dd-sdk-ios/issues/415 +[#419]: https://github.com/DataDog/dd-sdk-ios/issues/419 +[#421]: https://github.com/DataDog/dd-sdk-ios/issues/421 +[#423]: https://github.com/DataDog/dd-sdk-ios/issues/423 +[#431]: https://github.com/DataDog/dd-sdk-ios/issues/431 +[#436]: https://github.com/DataDog/dd-sdk-ios/issues/436 +[#439]: https://github.com/DataDog/dd-sdk-ios/issues/439 +[#444]: https://github.com/DataDog/dd-sdk-ios/issues/444 +[#447]: https://github.com/DataDog/dd-sdk-ios/issues/447 +[#450]: https://github.com/DataDog/dd-sdk-ios/issues/450 +[#451]: https://github.com/DataDog/dd-sdk-ios/issues/451 +[#473]: https://github.com/DataDog/dd-sdk-ios/issues/473 +[#474]: https://github.com/DataDog/dd-sdk-ios/issues/474 +[#479]: https://github.com/DataDog/dd-sdk-ios/issues/479 +[#481]: https://github.com/DataDog/dd-sdk-ios/issues/481 +[#483]: https://github.com/DataDog/dd-sdk-ios/issues/483 +[#493]: https://github.com/DataDog/dd-sdk-ios/issues/493 +[#495]: https://github.com/DataDog/dd-sdk-ios/issues/495 +[#504]: https://github.com/DataDog/dd-sdk-ios/issues/504 +[#509]: https://github.com/DataDog/dd-sdk-ios/issues/509 +[#514]: https://github.com/DataDog/dd-sdk-ios/issues/514 +[#522]: https://github.com/DataDog/dd-sdk-ios/issues/522 +[#523]: https://github.com/DataDog/dd-sdk-ios/issues/523 +[#524]: https://github.com/DataDog/dd-sdk-ios/issues/524 +[#525]: https://github.com/DataDog/dd-sdk-ios/issues/525 +[#531]: https://github.com/DataDog/dd-sdk-ios/issues/531 +[#534]: https://github.com/DataDog/dd-sdk-ios/issues/534 +[#535]: https://github.com/DataDog/dd-sdk-ios/issues/535 +[#537]: https://github.com/DataDog/dd-sdk-ios/issues/537 +[#539]: https://github.com/DataDog/dd-sdk-ios/issues/539 +[#545]: https://github.com/DataDog/dd-sdk-ios/issues/545 +[#547]: https://github.com/DataDog/dd-sdk-ios/issues/547 +[#562]: https://github.com/DataDog/dd-sdk-ios/issues/562 +[#563]: https://github.com/DataDog/dd-sdk-ios/issues/563 +[#566]: https://github.com/DataDog/dd-sdk-ios/issues/566 +[#567]: https://github.com/DataDog/dd-sdk-ios/issues/567 +[#569]: https://github.com/DataDog/dd-sdk-ios/issues/569 +[#575]: https://github.com/DataDog/dd-sdk-ios/issues/575 +[#576]: https://github.com/DataDog/dd-sdk-ios/issues/576 +[#582]: https://github.com/DataDog/dd-sdk-ios/issues/582 +[#583]: https://github.com/DataDog/dd-sdk-ios/issues/583 +[#590]: https://github.com/DataDog/dd-sdk-ios/issues/590 +[#605]: https://github.com/DataDog/dd-sdk-ios/issues/605 +[#608]: https://github.com/DataDog/dd-sdk-ios/issues/608 +[#609]: https://github.com/DataDog/dd-sdk-ios/issues/609 +[#613]: https://github.com/DataDog/dd-sdk-ios/issues/613 +[#615]: https://github.com/DataDog/dd-sdk-ios/issues/615 +[#619]: https://github.com/DataDog/dd-sdk-ios/issues/619 +[#623]: https://github.com/DataDog/dd-sdk-ios/issues/623 +[#626]: https://github.com/DataDog/dd-sdk-ios/issues/626 +[#627]: https://github.com/DataDog/dd-sdk-ios/issues/627 +[#640]: https://github.com/DataDog/dd-sdk-ios/issues/640 +[#641]: https://github.com/DataDog/dd-sdk-ios/issues/641 +[#644]: https://github.com/DataDog/dd-sdk-ios/issues/644 +[#654]: https://github.com/DataDog/dd-sdk-ios/issues/654 +[#655]: https://github.com/DataDog/dd-sdk-ios/issues/655 +[#676]: https://github.com/DataDog/dd-sdk-ios/issues/676 +[#679]: https://github.com/DataDog/dd-sdk-ios/issues/679 +[#680]: https://github.com/DataDog/dd-sdk-ios/issues/680 +[#692]: https://github.com/DataDog/dd-sdk-ios/issues/692 +[#699]: https://github.com/DataDog/dd-sdk-ios/issues/699 +[#702]: https://github.com/DataDog/dd-sdk-ios/issues/702 +[#708]: https://github.com/DataDog/dd-sdk-ios/issues/708 +[#712]: https://github.com/DataDog/dd-sdk-ios/issues/712 +[#715]: https://github.com/DataDog/dd-sdk-ios/issues/715 +[#724]: https://github.com/DataDog/dd-sdk-ios/issues/724 +[#725]: https://github.com/DataDog/dd-sdk-ios/issues/725 +[#728]: https://github.com/DataDog/dd-sdk-ios/issues/728 +[#729]: https://github.com/DataDog/dd-sdk-ios/issues/729 +[#761]: https://github.com/DataDog/dd-sdk-ios/issues/761 +[#789]: https://github.com/DataDog/dd-sdk-ios/issues/789 +[#793]: https://github.com/DataDog/dd-sdk-ios/issues/793 +[#794]: https://github.com/DataDog/dd-sdk-ios/issues/794 +[#795]: https://github.com/DataDog/dd-sdk-ios/issues/795 +[#797]: https://github.com/DataDog/dd-sdk-ios/issues/797 +[#815]: https://github.com/DataDog/dd-sdk-ios/issues/815 +[#830]: https://github.com/DataDog/dd-sdk-ios/issues/830 +[#832]: https://github.com/DataDog/dd-sdk-ios/issues/832 +[#837]: https://github.com/DataDog/dd-sdk-ios/issues/837 +[#851]: https://github.com/DataDog/dd-sdk-ios/issues/851 +[#867]: https://github.com/DataDog/dd-sdk-ios/issues/867 +[#876]: https://github.com/DataDog/dd-sdk-ios/issues/876 +[#888]: https://github.com/DataDog/dd-sdk-ios/issues/888 +[#894]: https://github.com/DataDog/dd-sdk-ios/issues/894 +[#949]: https://github.com/DataDog/dd-sdk-ios/issues/949 +[#950]: https://github.com/DataDog/dd-sdk-ios/issues/950 +[#964]: https://github.com/DataDog/dd-sdk-ios/issues/964 +[#973]: https://github.com/DataDog/dd-sdk-ios/issues/973 +[#997]: https://github.com/DataDog/dd-sdk-ios/issues/997 +[#1007]: https://github.com/DataDog/dd-sdk-ios/issues/1007 +[#1013]: https://github.com/DataDog/dd-sdk-ios/issues/1013 +[#1029]: https://github.com/DataDog/dd-sdk-ios/issues/1029 +[#1031]: https://github.com/DataDog/dd-sdk-ios/issues/1031 +[#1043]: https://github.com/DataDog/dd-sdk-ios/issues/1043 +[#1045]: https://github.com/DataDog/dd-sdk-ios/pull/1045 +[#1051]: https://github.com/DataDog/dd-sdk-ios/pull/1051 +[#1057]: https://github.com/DataDog/dd-sdk-ios/pull/1057 +[#1061]: https://github.com/DataDog/dd-sdk-ios/pull/1061 +[#1071]: https://github.com/DataDog/dd-sdk-ios/pull/1071 +[#1089]: https://github.com/DataDog/dd-sdk-ios/pull/1089 +[#1145]: https://github.com/DataDog/dd-sdk-ios/pull/1145 +[#1160]: https://github.com/DataDog/dd-sdk-ios/pull/1160 +[#1177]: https://github.com/DataDog/dd-sdk-ios/pull/1177 +[#1188]: https://github.com/DataDog/dd-sdk-ios/pull/1188 +[#1209]: https://github.com/DataDog/dd-sdk-ios/pull/1209 +[#1216]: https://github.com/DataDog/dd-sdk-ios/pull/1216 +[#1219]: https://github.com/DataDog/dd-sdk-ios/pull/1219 +[#1220]: https://github.com/DataDog/dd-sdk-ios/pull/1220 +[#1247]: https://github.com/DataDog/dd-sdk-ios/pull/1247 +[#1250]: https://github.com/DataDog/dd-sdk-ios/pull/1250 +[#1259]: https://github.com/DataDog/dd-sdk-ios/pull/1259 +[#1264]: https://github.com/DataDog/dd-sdk-ios/pull/1264 +[#1272]: https://github.com/DataDog/dd-sdk-ios/pull/1272 +[#1311]: https://github.com/DataDog/dd-sdk-ios/pull/1311 +[#1315]: https://github.com/DataDog/dd-sdk-ios/pull/1315 +[#1331]: https://github.com/DataDog/dd-sdk-ios/pull/1331 +[#1328]: https://github.com/DataDog/dd-sdk-ios/pull/1328 +[#1355]: https://github.com/DataDog/dd-sdk-ios/pull/1355 +[#1410]: https://github.com/DataDog/dd-sdk-ios/pull/1410 +[#1412]: https://github.com/DataDog/dd-sdk-ios/pull/1412 +[#1413]: https://github.com/DataDog/dd-sdk-ios/pull/1413 +[#1415]: https://github.com/DataDog/dd-sdk-ios/pull/1415 +[#1418]: https://github.com/DataDog/dd-sdk-ios/pull/1418 +[#1419]: https://github.com/DataDog/dd-sdk-ios/pull/1419 +[#1428]: https://github.com/DataDog/dd-sdk-ios/pull/1428 +[#1444]: https://github.com/DataDog/dd-sdk-ios/pull/1444 +[#1464]: https://github.com/DataDog/dd-sdk-ios/pull/1464 +[#1412]: https://github.com/DataDog/dd-sdk-ios/pull/1412 +[#1488]: https://github.com/DataDog/dd-sdk-ios/pull/1488 +[#1502]: https://github.com/DataDog/dd-sdk-ios/pull/1502 +[#1515]: https://github.com/DataDog/dd-sdk-ios/pull/1515 +[#1465]: https://github.com/DataDog/dd-sdk-ios/pull/1465 +[#1498]: https://github.com/DataDog/dd-sdk-ios/pull/1498 +[#1493]: https://github.com/DataDog/dd-sdk-ios/pull/1493 +[#1394]: https://github.com/DataDog/dd-sdk-ios/pull/1394 +[#1524]: https://github.com/DataDog/dd-sdk-ios/pull/1524 +[#1529]: https://github.com/DataDog/dd-sdk-ios/pull/1529 +[#1533]: https://github.com/DataDog/dd-sdk-ios/pull/1533 +[#1645]: https://github.com/DataDog/dd-sdk-ios/pull/1645 +[#1594]: https://github.com/DataDog/dd-sdk-ios/pull/1594 +[#1536]: https://github.com/DataDog/dd-sdk-ios/pull/1536 +[#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609 +[#1639]: https://github.com/DataDog/dd-sdk-ios/pull/1639 +[#1615]: https://github.com/DataDog/dd-sdk-ios/pull/1615 +[#1531]: https://github.com/DataDog/dd-sdk-ios/pull/1531 +[#1637]: https://github.com/DataDog/dd-sdk-ios/pull/1637 +[#1541]: https://github.com/DataDog/dd-sdk-ios/pull/1541 +[#1592]: https://github.com/DataDog/dd-sdk-ios/pull/1592 +[#1672]: https://github.com/DataDog/dd-sdk-ios/pull/1672 +[#1596]: https://github.com/DataDog/dd-sdk-ios/pull/1596 +[#1597]: https://github.com/DataDog/dd-sdk-ios/pull/1597 +[#1627]: https://github.com/DataDog/dd-sdk-ios/pull/1627 +[#1644]: https://github.com/DataDog/dd-sdk-ios/pull/1644 +[#1685]: https://github.com/DataDog/dd-sdk-ios/pull/1685 +[#1656]: https://github.com/DataDog/dd-sdk-ios/pull/1656 +[#1666]: https://github.com/DataDog/dd-sdk-ios/pull/1666 +[#1696]: https://github.com/DataDog/dd-sdk-ios/pull/1696 +[#1697]: https://github.com/DataDog/dd-sdk-ios/pull/1697 +[#1707]: https://github.com/DataDog/dd-sdk-ios/pull/1707 +[#1711]: https://github.com/DataDog/dd-sdk-ios/pull/1711 +[#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 +[#1722]: https://github.com/DataDog/dd-sdk-ios/pull/1722 +[#1724]: https://github.com/DataDog/dd-sdk-ios/pull/1724 +[#1741]: https://github.com/DataDog/dd-sdk-ios/pull/1741 +[#1742]: https://github.com/DataDog/dd-sdk-ios/pull/1742 +[#1746]: https://github.com/DataDog/dd-sdk-ios/pull/1746 +[#1747]: https://github.com/DataDog/dd-sdk-ios/pull/1747 +[#1794]: https://github.com/DataDog/dd-sdk-ios/pull/1794 +[#1774]: https://github.com/DataDog/dd-sdk-ios/pull/1774 +[#1763]: https://github.com/DataDog/dd-sdk-ios/pull/1763 +[#1767]: https://github.com/DataDog/dd-sdk-ios/pull/1767 +[#1843]: https://github.com/DataDog/dd-sdk-ios/pull/1843 +[#1798]: https://github.com/DataDog/dd-sdk-ios/pull/1798 +[#1891]: https://github.com/DataDog/dd-sdk-ios/pull/1891 +[#1776]: https://github.com/DataDog/dd-sdk-ios/pull/1776 +[#1834]: https://github.com/DataDog/dd-sdk-ios/pull/1834 +[#1721]: https://github.com/DataDog/dd-sdk-ios/pull/1721 +[#1803]: https://github.com/DataDog/dd-sdk-ios/pull/1803 +[#1853]: https://github.com/DataDog/dd-sdk-ios/pull/1853 +[#1807]: https://github.com/DataDog/dd-sdk-ios/pull/1807 +[#1854]: https://github.com/DataDog/dd-sdk-ios/pull/1854 +[#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 +[@leffelmania]: https://github.com/LeffelMania +[@simpleapp]: https://github.com/SimpleApp +[@tsvetelinvladimirov]: https://github.com/TsvetelinVladimirov +[@arnauddorgans]: https://github.com/arnauddorgans +[@ben-yolabs]: https://github.com/ben-yolabs +[@earltedly]: https://github.com/earltedly +[@flobories]: https://github.com/flobories +[@hyling]: https://github.com/hyling +[@jegnux]: https://github.com/jegnux +[@joeydong]: https://github.com/joeydong +[@jracollins]: https://github.com/jracollins +[@lgaches]: https://github.com/lgaches +[@lmramirez]: https://github.com/lmramirez +[@marcusway]: https://github.com/marcusway +[@aldoKelvianto]: https://github.com/aldoKelvianto +[@matcartmill]: https://github.com/matcartmill +[@michalsrutek]: https://github.com/michalsrutek +[@philtre]: https://github.com/philtre +[@pingd]: https://github.com/pingd +[@provtheodorenewell]: https://github.com/provTheodoreNewell +[@safa-ads]: https://github.com/safa-ads +[@sdejesusf]: https://github.com/sdejesusF +[@avdlee]: https://github.com/AvdLee +[@dfed]: https://github.com/dfed +[@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/CONTRIBUTING.md b/CONTRIBUTING.md index 6959ee72ee..0caa069e9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,8 @@ First of all, thanks for contributing! This document provides some basic guidelines for contributing to this repository. To propose improvements, feel free to submit a PR or open an Issue. +**Note:** Datadog requires that all commits within this repository must be signed, including those within external contribution PRs. Please ensure you have followed GitHub's [Signing Commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) guide before proposing a contribution. PRs lacking signed commits will not be processed and may be rejected. + ## Have a feature request or idea? Many great ideas for new features come from the community, and we'd be happy to consider yours 👍. @@ -50,7 +52,21 @@ The workspace for SDK development and integration (tests, benchmarks, example ap #### Tests -`DatadogTests` (unit tests), `DatadogIntegrationTests` (integration tests), and `DatadogBenchmarkTests` (benchmarks) source files +`DatadogTests` (unit tests), `IntegrationTests`, and `DatadogBenchmarkTests` (benchmarks) source files + +#### Lint + +We're using `swiftlint` to ensure our codebase follows Swift standard syntax. You can run the lint with our custom rules with the following command line: + +```shell +$ ./tools/lint/run-linter.sh +``` + +In order to apply automatic correction of violations use `--fix` flag: + +```shell +$ ./tools/lint/run-linter.sh --fix +``` #### Dependency manager tests diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000000..e3155c952c --- /dev/null +++ b/Cartfile @@ -0,0 +1,2 @@ +github "microsoft/plcrashreporter" ~> 1.11.2 +binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" == 1.6.0 diff --git a/Cartfile.resolved b/Cartfile.resolved new file mode 100644 index 0000000000..613647a075 --- /dev/null +++ b/Cartfile.resolved @@ -0,0 +1,2 @@ +binary "https://raw.githubusercontent.com/DataDog/opentelemetry-swift-packages/main/OpenTelemetryApi.json" "1.6.0" +github "microsoft/plcrashreporter" "1.11.2" 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 2cd18f157b..8c1db17e4b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -3,59 +3,293 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 61133B8C242393DE00786299 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; }; - 61133B93242393DE00786299 /* Datadog.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133B85242393DE00786299 /* Datadog.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 61133BCA2423979B00786299 /* EncodableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA02423979B00786299 /* EncodableValue.swift */; }; - 61133BCB2423979B00786299 /* CarrierInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA22423979B00786299 /* CarrierInfoProvider.swift */; }; - 61133BCC2423979B00786299 /* MobileDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA32423979B00786299 /* MobileDevice.swift */; }; - 61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA42423979B00786299 /* NetworkConnectionInfoProvider.swift */; }; - 61133BCE2423979B00786299 /* BatteryStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA52423979B00786299 /* BatteryStatusProvider.swift */; }; + 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 */; }; + 3C0D5DE32A543DC900446CF9 /* EventGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */; }; + 3C0D5DE42A543E3400446CF9 /* EventGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */; }; + 3C0D5DE52A543E3500446CF9 /* EventGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */; }; + 3C0D5DE92A543EA200446CF9 /* RUMViewEventsFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DE62A543E9700446CF9 /* RUMViewEventsFilterTests.swift */; }; + 3C0D5DEA2A543EA300446CF9 /* RUMViewEventsFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DE62A543E9700446CF9 /* RUMViewEventsFilterTests.swift */; }; + 3C0D5DEC2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DEB2A54405A00446CF9 /* RUMViewEventsFilter.swift */; }; + 3C0D5DED2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DEB2A54405A00446CF9 /* RUMViewEventsFilter.swift */; }; + 3C0D5DEF2A5442A900446CF9 /* EventMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */; }; + 3C0D5DF02A5442A900446CF9 /* EventMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */; }; + 3C0D5DF52A5443B100446CF9 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */; }; + 3C0D5DF62A5443B100446CF9 /* DataFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */; }; + 3C1890152ABDE9BF00CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */; }; + 3C1890162ABDE9C000CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */; }; + 3C2206F52AB9DB9000DE780C /* DatadogSessionReplay.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; }; + 3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */; }; + 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 */; }; + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */; }; + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */; }; + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */; }; + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */; }; + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */; }; + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */; }; + 3C74305C29FBC0480053B80F /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */; }; + 3C85D42A29F7C70300AFF894 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + 3C85D42C29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; + 3C85D42D29F7C87D00AFF894 /* HostsSanitizerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */; }; + 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 */; }; + 3CA852652BF2148400B52CBA /* TraceContextInjection+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */; }; + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */; }; + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */; }; + 3CBDE6742AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; + 3CBDE6752AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */; }; + 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */; }; + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */; }; + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */; }; + 3CCCA5C42ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; + 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */; }; + 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; + 3CCCA5C82ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */; }; + 3CCECDAF2BC688120013C125 /* SpanIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */; }; + 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 */; }; + 49274907288048B800ECD49B /* InternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274903288048AA00ECD49B /* InternalProxyTests.swift */; }; + 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */; }; + 49D8C0B82AC5D2160075E427 /* RUM+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */; }; + 49D8C0BD2AC5F2BB0075E427 /* Logs+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B92AC5F21F0075E427 /* Logs+Internal.swift */; }; + 49D8C0BE2AC5F2BC0075E427 /* Logs+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D8C0B92AC5F21F0075E427 /* Logs+Internal.swift */; }; + 61020C2A2757AD91005EEAEA /* BackgroundLocationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61020C292757AD91005EEAEA /* BackgroundLocationMonitor.swift */; }; + 61020C2C2758E853005EEAEA /* DebugBackgroundEventsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61020C2B2758E853005EEAEA /* DebugBackgroundEventsViewController.swift */; }; + 61054E612A6EE10A00AAA894 /* SRCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E082A6EE10A00AAA894 /* SRCompression.swift */; }; + 61054E622A6EE10A00AAA894 /* RecordWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E092A6EE10A00AAA894 /* RecordWriter.swift */; }; + 61054E632A6EE10A00AAA894 /* SessionReplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E0B2A6EE10A00AAA894 /* SessionReplayConfiguration.swift */; }; + 61054E642A6EE10A00AAA894 /* SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E0C2A6EE10A00AAA894 /* SessionReplay.swift */; }; + 61054E652A6EE10A00AAA894 /* AppWindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E0F2A6EE10A00AAA894 /* AppWindowObserver.swift */; }; + 61054E662A6EE10A00AAA894 /* KeyWindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E102A6EE10A00AAA894 /* KeyWindowObserver.swift */; }; + 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 /* 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+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 */; }; + 61054E712A6EE10A00AAA894 /* TouchSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1E2A6EE10A00AAA894 /* TouchSnapshot.swift */; }; + 61054E722A6EE10A00AAA894 /* TouchIdentifierGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1F2A6EE10A00AAA894 /* TouchIdentifierGenerator.swift */; }; + 61054E732A6EE10A00AAA894 /* WindowTouchSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E202A6EE10A00AAA894 /* WindowTouchSnapshotProducer.swift */; }; + 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E222A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift */; }; + 61054E752A6EE10A00AAA894 /* ViewTreeSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */; }; + 61054E762A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */; }; + 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */; }; + 61054E782A6EE10A00AAA894 /* UIDatePickerRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E282A6EE10A00AAA894 /* UIDatePickerRecorder.swift */; }; + 61054E792A6EE10A00AAA894 /* UITextViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E292A6EE10A00AAA894 /* UITextViewRecorder.swift */; }; + 61054E7A2A6EE10A00AAA894 /* UIImageViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2A2A6EE10A00AAA894 /* UIImageViewRecorder.swift */; }; + 61054E7B2A6EE10A00AAA894 /* UIViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2B2A6EE10A00AAA894 /* UIViewRecorder.swift */; }; + 61054E7C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift */; }; + 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2D2A6EE10A00AAA894 /* UITextFieldRecorder.swift */; }; + 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2E2A6EE10A00AAA894 /* NodeRecorder.swift */; }; + 61054E7F2A6EE10A00AAA894 /* UISliderRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E2F2A6EE10A00AAA894 /* UISliderRecorder.swift */; }; + 61054E802A6EE10A00AAA894 /* UIPickerViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E302A6EE10A00AAA894 /* UIPickerViewRecorder.swift */; }; + 61054E812A6EE10A00AAA894 /* UIStepperRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E312A6EE10A00AAA894 /* UIStepperRecorder.swift */; }; + 61054E822A6EE10A00AAA894 /* UILabelRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E322A6EE10A00AAA894 /* UILabelRecorder.swift */; }; + 61054E832A6EE10A00AAA894 /* UISwitchRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E332A6EE10A00AAA894 /* UISwitchRecorder.swift */; }; + 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 */; }; + 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 */; }; + 61054E8B2A6EE10A00AAA894 /* SessionReplayFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3C2A6EE10A00AAA894 /* SessionReplayFeature.swift */; }; + 61054E8D2A6EE10A00AAA894 /* RUMContextReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3E2A6EE10A00AAA894 /* RUMContextReceiver.swift */; }; + 61054E8E2A6EE10A00AAA894 /* SRContextPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3F2A6EE10A00AAA894 /* SRContextPublisher.swift */; }; + 61054E8F2A6EE10A00AAA894 /* SegmentRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E412A6EE10A00AAA894 /* SegmentRequestBuilder.swift */; }; + 61054E902A6EE10A00AAA894 /* SegmentJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E432A6EE10A00AAA894 /* SegmentJSON.swift */; }; + 61054E932A6EE10A00AAA894 /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E472A6EE10A00AAA894 /* MultipartFormData.swift */; }; + 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4A2A6EE10A00AAA894 /* TextObfuscator.swift */; }; + 61054E952A6EE10A00AAA894 /* SnapshotProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */; }; + 61054E962A6EE10A00AAA894 /* Diff+SRWireframes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4D2A6EE10A00AAA894 /* Diff+SRWireframes.swift */; }; + 61054E972A6EE10A00AAA894 /* Diff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E4E2A6EE10A00AAA894 /* Diff.swift */; }; + 61054E982A6EE10A00AAA894 /* RecordsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */; }; + 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 */; }; + 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+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 */; }; + 61054F9B2A6EE1BA00AAA894 /* QueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F442A6EE1B900AAA894 /* QueueTests.swift */; }; + 61054F9C2A6EE1BA00AAA894 /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F452A6EE1B900AAA894 /* SwiftExtensionsTests.swift */; }; + 61054F9D2A6EE1BA00AAA894 /* MainThreadSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F472A6EE1B900AAA894 /* MainThreadSchedulerTests.swift */; }; + 61054F9E2A6EE1BA00AAA894 /* SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F482A6EE1B900AAA894 /* SessionReplayTests.swift */; }; + 61054F9F2A6EE1BA00AAA894 /* RecordsWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F4A2A6EE1BA00AAA894 /* RecordsWriterTests.swift */; }; + 61054FA02A6EE1BA00AAA894 /* SRCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F4B2A6EE1BA00AAA894 /* SRCompressionTests.swift */; }; + 61054FA22A6EE1BA00AAA894 /* TextObfuscatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F502A6EE1BA00AAA894 /* TextObfuscatorTests.swift */; }; + 61054FA32A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F522A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift */; }; + 61054FA42A6EE1BA00AAA894 /* DiffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F532A6EE1BA00AAA894 /* DiffTests.swift */; }; + 61054FA52A6EE1BA00AAA894 /* RecordsBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */; }; + 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 /* 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 */; }; + 61054FB02A6EE1BA00AAA894 /* ViewTreeSnapshotBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F672A6EE1BA00AAA894 /* ViewTreeSnapshotBuilderTests.swift */; }; + 61054FB12A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F682A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift */; }; + 61054FB22A6EE1BA00AAA894 /* UILabelRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6A2A6EE1BA00AAA894 /* UILabelRecorderTests.swift */; }; + 61054FB32A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6B2A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift */; }; + 61054FB42A6EE1BA00AAA894 /* UITabBarRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6C2A6EE1BA00AAA894 /* UITabBarRecorderTests.swift */; }; + 61054FB52A6EE1BA00AAA894 /* UISliderRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6D2A6EE1BA00AAA894 /* UISliderRecorderTests.swift */; }; + 61054FB62A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6E2A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift */; }; + 61054FB72A6EE1BA00AAA894 /* UISegmentRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F6F2A6EE1BA00AAA894 /* UISegmentRecorderTests.swift */; }; + 61054FB82A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F702A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift */; }; + 61054FB92A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F712A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift */; }; + 61054FBA2A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F722A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift */; }; + 61054FBB2A6EE1BA00AAA894 /* UISwitchRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F732A6EE1BA00AAA894 /* UISwitchRecorderTests.swift */; }; + 61054FBC2A6EE1BA00AAA894 /* UIStepperRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F742A6EE1BA00AAA894 /* UIStepperRecorderTests.swift */; }; + 61054FBD2A6EE1BA00AAA894 /* UIViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F752A6EE1BA00AAA894 /* UIViewRecorderTests.swift */; }; + 61054FBE2A6EE1BA00AAA894 /* UIImageViewWireframesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F762A6EE1BA00AAA894 /* UIImageViewWireframesBuilderTests.swift */; }; + 61054FBF2A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F772A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift */; }; + 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 /* 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 */; }; + 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */; }; + 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */; }; + 61054FC92A6EE1BA00AAA894 /* RecorderMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */; }; + 61054FCA2A6EE1BA00AAA894 /* TestScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */; }; + 61054FCB2A6EE1BA00AAA894 /* QueueMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */; }; + 61054FCD2A6EE1BA00AAA894 /* SnapshotProducerMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F862A6EE1BA00AAA894 /* SnapshotProducerMocks.swift */; }; + 61054FCE2A6EE1BA00AAA894 /* RUMContextObserverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */; }; + 61054FCF2A6EE1BA00AAA894 /* RUMContextReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F892A6EE1BA00AAA894 /* RUMContextReceiverTests.swift */; }; + 61054FD02A6EE1BA00AAA894 /* SRContextPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F8A2A6EE1BA00AAA894 /* SRContextPublisherTests.swift */; }; + 61054FD32A6EE1BA00AAA894 /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F902A6EE1BA00AAA894 /* MultipartFormDataTests.swift */; }; + 61054FD42A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F912A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift */; }; + 61054FD52A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F932A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift */; }; + 610ABD4C2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */; }; + 610ABD4D2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */; }; + 61112F8E2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */; }; + 61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */; }; + 6111C58225C0081F00F5C4A2 /* RUMDataModels+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */; }; + 61133B8C242393DE00786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; + 61133B93242393DE00786299 /* DatadogCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133B85242393DE00786299 /* DatadogCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61133BCF2423979B00786299 /* FileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA72423979B00786299 /* FileWriter.swift */; }; - 61133BD02423979B00786299 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA82423979B00786299 /* DateProvider.swift */; }; 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA92423979B00786299 /* FilesOrchestrator.swift */; }; - 61133BD22423979B00786299 /* Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAB2423979B00786299 /* Directory.swift */; }; - 61133BD32423979B00786299 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAC2423979B00786299 /* File.swift */; }; 61133BD42423979B00786299 /* FileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAD2423979B00786299 /* FileReader.swift */; }; 61133BD52423979B00786299 /* DataUploadConditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAF2423979B00786299 /* DataUploadConditions.swift */; }; 61133BD62423979B00786299 /* DataUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB02423979B00786299 /* DataUploader.swift */; }; 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB12423979B00786299 /* DataUploadWorker.swift */; }; - 61133BD82423979B00786299 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB22423979B00786299 /* HTTPClient.swift */; }; + 61133BD82423979B00786299 /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB22423979B00786299 /* URLSessionClient.swift */; }; 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB32423979B00786299 /* DataUploadDelay.swift */; }; - 61133BDA2423979B00786299 /* HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB42423979B00786299 /* HTTPHeaders.swift */; }; - 61133BDB2423979B00786299 /* DatadogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB52423979B00786299 /* DatadogConfiguration.swift */; }; - 61133BDC2423979B00786299 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB62423979B00786299 /* Logger.swift */; }; - 61133BDD2423979B00786299 /* InternalLoggers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB82423979B00786299 /* InternalLoggers.swift */; }; - 61133BDE2423979B00786299 /* CompilationConditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB92423979B00786299 /* CompilationConditions.swift */; }; - 61133BDF2423979B00786299 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BBA2423979B00786299 /* SwiftExtensions.swift */; }; - 61133BE02423979B00786299 /* Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BBB2423979B00786299 /* Datadog.swift */; }; - 61133BE32423979B00786299 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC02423979B00786299 /* UserInfo.swift */; }; - 61133BE42423979B00786299 /* LogEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC22423979B00786299 /* LogEncoder.swift */; }; - 61133BE52423979B00786299 /* LogBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC32423979B00786299 /* LogBuilder.swift */; }; - 61133BE62423979B00786299 /* LogSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC42423979B00786299 /* LogSanitizer.swift */; }; - 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC62423979B00786299 /* LogUtilityOutputs.swift */; }; - 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC72423979B00786299 /* LogFileOutput.swift */; }; - 61133BE92423979B00786299 /* LogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC82423979B00786299 /* LogOutput.swift */; }; - 61133BEA2423979B00786299 /* LogConsoleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC92423979B00786299 /* LogConsoleOutput.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 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0B2423983800786299 /* AnyEncodable.swift */; }; - 61133C102423983800786299 /* Logger+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0C2423983800786299 /* Logger+objc.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 */; }; - 61133C492423990D00786299 /* DDLoggerBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */; }; 61133C4A2423990D00786299 /* DDConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C162423990D00786299 /* DDConfigurationTests.swift */; }; - 61133C4B2423990D00786299 /* DDLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C172423990D00786299 /* DDLoggerTests.swift */; }; + 61133C4B2423990D00786299 /* DDLogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C172423990D00786299 /* DDLogsTests.swift */; }; 61133C4D2423990D00786299 /* CoreTelephonyMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */; }; 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1C2423990D00786299 /* UIKitMocks.swift */; }; - 61133C522423990D00786299 /* FoundationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C202423990D00786299 /* FoundationMocks.swift */; }; - 61133C532423990D00786299 /* MobileDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C232423990D00786299 /* MobileDeviceTests.swift */; }; - 61133C542423990D00786299 /* NetworkConnectionInfoProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C242423990D00786299 /* NetworkConnectionInfoProviderTests.swift */; }; - 61133C552423990D00786299 /* BatteryStatusProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C252423990D00786299 /* BatteryStatusProviderTests.swift */; }; - 61133C562423990D00786299 /* CarrierInfoProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C262423990D00786299 /* CarrierInfoProviderTests.swift */; }; 61133C572423990D00786299 /* FileReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C282423990D00786299 /* FileReaderTests.swift */; }; 61133C582423990D00786299 /* FileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C292423990D00786299 /* FileWriterTests.swift */; }; 61133C592423990D00786299 /* FilesOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2A2423990D00786299 /* FilesOrchestratorTests.swift */; }; @@ -63,139 +297,1515 @@ 61133C5B2423990D00786299 /* DirectoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2D2423990D00786299 /* DirectoryTests.swift */; }; 61133C5C2423990D00786299 /* DataUploadWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */; }; 61133C5D2423990D00786299 /* DataUploadConditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C302423990D00786299 /* DataUploadConditionsTests.swift */; }; - 61133C5E2423990D00786299 /* LogsUploadDelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C312423990D00786299 /* LogsUploadDelayTests.swift */; }; + 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C312423990D00786299 /* DataUploadDelayTests.swift */; }; 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C322423990D00786299 /* DataUploaderTests.swift */; }; - 61133C602423990D00786299 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C332423990D00786299 /* HTTPHeadersTests.swift */; }; - 61133C612423990D00786299 /* HTTPClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C342423990D00786299 /* HTTPClientTests.swift */; }; - 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C362423990D00786299 /* InternalLoggersTests.swift */; }; + 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C332423990D00786299 /* RequestBuilderTests.swift */; }; + 61133C612423990D00786299 /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C342423990D00786299 /* URLSessionClientTests.swift */; }; 61133C642423990D00786299 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C382423990D00786299 /* LoggerTests.swift */; }; - 61133C652423990D00786299 /* LogBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3B2423990D00786299 /* LogBuilderTests.swift */; }; - 61133C662423990D00786299 /* LogSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3C2423990D00786299 /* LogSanitizerTests.swift */; }; - 61133C672423990D00786299 /* LogConsoleOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3E2423990D00786299 /* LogConsoleOutputTests.swift */; }; - 61133C682423990D00786299 /* LogUtilityOutputsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3F2423990D00786299 /* LogUtilityOutputsTests.swift */; }; - 61133C692423990D00786299 /* LogFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C402423990D00786299 /* LogFileOutputTests.swift */; }; 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C412423990D00786299 /* DatadogTests.swift */; }; 61133C6B2423990D00786299 /* LogMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C432423990D00786299 /* LogMatcher.swift */; }; - 61133C6C2423990D00786299 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C452423990D00786299 /* SwiftExtensions.swift */; }; - 61133C6D2423990D00786299 /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; 61133C6E2423990D00786299 /* DatadogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C472423990D00786299 /* DatadogExtensions.swift */; }; - 61133C702423993200786299 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; }; - 61133C712423993200786299 /* Datadog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 61216276247D1CD700AC5D67 /* LoggingForTracingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */; }; - 6121627C247D220500AC5D67 /* LoggingForTracingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */; }; - 612983CD2449E62E00D4424B /* LoggingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612983CC2449E62E00D4424B /* LoggingFeature.swift */; }; + 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 */; }; + 61216B802667C79B0089DCD1 /* LogsTrackingConsentE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B7F2667C79B0089DCD1 /* LogsTrackingConsentE2ETests.swift */; }; + 61216B842667CFF70089DCD1 /* DatadogE2EHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B7D2667BC220089DCD1 /* DatadogE2EHelpers.swift */; }; + 61216B852667CFFE0089DCD1 /* RUME2EHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216B7826679DD20089DCD1 /* RUME2EHelpers.swift */; }; + 612556B0268C8D31002BCE74 /* CrashReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612556AF268C8D31002BCE74 /* CrashReport.swift */; }; + 612556BB268DD9BF002BCE74 /* DDCrashReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612556BA268DD9BF002BCE74 /* DDCrashReportExporter.swift */; }; + 6128F56A2BA2237300D35B08 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5692BA2237300D35B08 /* DataStore.swift */; }; + 6128F56B2BA2237300D35B08 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5692BA2237300D35B08 /* DataStore.swift */; }; + 6128F56E2BA223A100D35B08 /* FeatureDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F56D2BA223A100D35B08 /* FeatureDataStore.swift */; }; + 6128F56F2BA223A100D35B08 /* FeatureDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F56D2BA223A100D35B08 /* FeatureDataStore.swift */; }; + 6128F5712BA223D100D35B08 /* DataStore+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5702BA223D100D35B08 /* DataStore+TLV.swift */; }; + 6128F5722BA223D100D35B08 /* DataStore+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5702BA223D100D35B08 /* DataStore+TLV.swift */; }; + 6128F5742BA3280300D35B08 /* DataStoreFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5732BA3280300D35B08 /* DataStoreFileReader.swift */; }; + 6128F5752BA3280300D35B08 /* DataStoreFileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5732BA3280300D35B08 /* DataStoreFileReader.swift */; }; + 6128F5772BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5762BA32DE500D35B08 /* DataStoreFileWriter.swift */; }; + 6128F5782BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5762BA32DE500D35B08 /* DataStoreFileWriter.swift */; }; + 6128F57B2BA35D6200D35B08 /* FeatureDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F57A2BA35D6200D35B08 /* FeatureDataStoreTests.swift */; }; + 6128F57C2BA35D6200D35B08 /* FeatureDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F57A2BA35D6200D35B08 /* FeatureDataStoreTests.swift */; }; + 6128F57E2BA8A3A000D35B08 /* DataStore+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F57D2BA8A3A000D35B08 /* DataStore+TLVTests.swift */; }; + 6128F57F2BA8A3A000D35B08 /* DataStore+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F57D2BA8A3A000D35B08 /* DataStore+TLVTests.swift */; }; + 6128F5842BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5832BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift */; }; + 6128F5852BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5832BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift */; }; + 6128F58A2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5892BA9860B00D35B08 /* DataStoreFileReaderTests.swift */; }; + 6128F58B2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6128F5892BA9860B00D35B08 /* DataStoreFileReaderTests.swift */; }; + 612C13D02AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13CF2AA772FA0086B5D1 /* SRRequestMatcher.swift */; }; + 612C13D12AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13CF2AA772FA0086B5D1 /* SRRequestMatcher.swift */; }; + 612C13D62AAB35EB0086B5D1 /* SRSegmentMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13D52AAB35EB0086B5D1 /* SRSegmentMatcher.swift */; }; + 612C13D72AAB35EB0086B5D1 /* SRSegmentMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13D52AAB35EB0086B5D1 /* SRSegmentMatcher.swift */; }; 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */; }; - 6132BF4424A3AAD700D7BD17 /* OTGlobal+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */; }; 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */; }; 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */; }; 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */; }; - 6132BF4E24A49D5400D7BD17 /* OTNoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */; }; 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF5024A49F7400D7BD17 /* Casting.swift */; }; + 6133D1EF2A6ED9E100384BEF /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + 6133D2012A6EDB7700384BEF /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + 6133D20B2A6EDBC100384BEF /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61345612244756E300E7DA6B /* PerformancePresetTests.swift */; }; - 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* AppDelegate.swift */; }; - 61441C0C24616DE9003D8BB8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main.storyboard */; }; + 6134CDB12A691E850061CCD9 /* BatchMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */; }; + 6134CDB22A691E850061CCD9 /* BatchMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */; }; + 61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */; }; + 6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */; }; + 6136CB4B2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */; }; + 6139CD712589FAFD007E8BB7 /* Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD702589FAFD007E8BB7 /* Retrying.swift */; }; + 6139CD772589FEE3007E8BB7 /* RetryingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */; }; + 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E792E2577B0F900DFCC17 /* Reader.swift */; }; + 613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E793A2577B6EE00DFCC17 /* DataReader.swift */; }; + 613F9C182BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C172BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift */; }; + 613F9C192BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C172BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift */; }; + 613F9C1B2BB03188007C7606 /* FeatureScopeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */; }; + 613F9C1C2BB03188007C7606 /* FeatureScopeMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */; }; + 614396722A67D74F00197326 /* BatchMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* BatchMetrics.swift */; }; + 614396732A67D74F00197326 /* BatchMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614396712A67D74F00197326 /* BatchMetrics.swift */; }; + 61441C0524616DE9003D8BB8 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; + 61441C0C24616DE9003D8BB8 /* Main iOS.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */; }; 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; - 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */; }; - 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3B24617013003D8BB8 /* IntegrationTests.swift */; }; - 61441C4124617013003D8BB8 /* LoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */; }; - 61441C44246174CE003D8BB8 /* HTTPServerMock in Frameworks */ = {isa = PBXBuildFile; productRef = 61441C43246174CE003D8BB8 /* HTTPServerMock */; }; - 61441C4924618052003D8BB8 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; }; - 61441C4A24618052003D8BB8 /* LogMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C432423990D00786299 /* LogMatcher.swift */; }; - 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; }; - 61441C4E24619498003D8BB8 /* Datadog.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 61441C6D24619FE4003D8BB8 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; platformFilter = ios; }; - 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */; }; - 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */; }; - 61441C7C2461A244003D8BB8 /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */; }; 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */; }; - 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */; }; 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */; }; 61441C992461A649003D8BB8 /* DebugLoggingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */; }; - 61441C9D2461A796003D8BB8 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C9C2461A796003D8BB8 /* AppConfig.swift */; }; - 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614872762485067300E3EBDB /* SpanTagsReducer.swift */; }; - 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614E9EB2244719FA007EE3E1 /* BundleType.swift */; }; + 614798962A459AA80095CB02 /* DDTraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614798952A459AA80095CB02 /* DDTraceTests.swift */; }; + 614798972A459AA80095CB02 /* DDTraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614798952A459AA80095CB02 /* DDTraceTests.swift */; }; + 614798992A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614798982A459B2E0095CB02 /* DDTraceConfigurationTests.swift */; }; + 6147989A2A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614798982A459B2E0095CB02 /* DDTraceConfigurationTests.swift */; }; + 6147989C2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6147989B2A459E2B0095CB02 /* DDTrace+apiTests.m */; }; + 6147989D2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6147989B2A459E2B0095CB02 /* DDTrace+apiTests.m */; }; + 6147989E2A45A42C0095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; + 614798A02A45A46B0095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; + 614798A22A45A48F0095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; + 614798A32A45A4980095CB02 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; + 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 */; }; + 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 */; }; + 614ED36C260352DC00C8C519 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; + 6156A9072BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */; }; + 6156A9082BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */; }; 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; - 61570006246AAE5E00E96950 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; - 615A4A8324A3431600233986 /* Tracer+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Tracer+objc.swift */; }; - 615A4A8524A3445700233986 /* TracerConfiguration+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */; }; - 615A4A8724A3452800233986 /* DDTracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */; }; + 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Trace+objc.swift */; }; 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8A24A3568900233986 /* OTSpan+objc.swift */; }; 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */; }; - 617CEB392456BC3A00AD4669 /* TracingUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CEB382456BC3A00AD4669 /* TracingUUID.swift */; }; + 615B0F8B2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615B0F8A2BB33C2800E9ED6C /* AppHangsMonitorTests.swift */; }; + 615B0F8C2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615B0F8A2BB33C2800E9ED6C /* AppHangsMonitorTests.swift */; }; + 615B0F8E2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615B0F8D2BB33E0400E9ED6C /* DataStoreMock.swift */; }; + 615B0F8F2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615B0F8D2BB33E0400E9ED6C /* DataStoreMock.swift */; }; + 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 */; }; + 6167E6D42B7F8B3300C3CA2D /* AppHangsMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D22B7F8B3300C3CA2D /* AppHangsMonitor.swift */; }; + 6167E6D62B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D52B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift */; }; + 6167E6D72B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D52B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift */; }; + 6167E6DA2B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D92B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift */; }; + 6167E6DB2B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6D92B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift */; }; + 6167E6DD2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */; }; + 6167E6DE2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */; }; + 6167E6E22B81207200C3CA2D /* DDCrashReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6E12B81207200C3CA2D /* DDCrashReport.swift */; }; + 6167E6E32B81207200C3CA2D /* DDCrashReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6E12B81207200C3CA2D /* DDCrashReport.swift */; }; + 6167E6E82B8122E900C3CA2D /* BacktraceReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */; }; + 6167E6E92B8122E900C3CA2D /* BacktraceReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */; }; + 6167E6F62B81E94C00C3CA2D /* DDThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6F52B81E94C00C3CA2D /* DDThread.swift */; }; + 6167E6F72B81E94C00C3CA2D /* DDThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6F52B81E94C00C3CA2D /* DDThread.swift */; }; + 6167E6F92B81E95900C3CA2D /* BinaryImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */; }; + 6167E6FA2B81E95900C3CA2D /* BinaryImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */; }; + 6167E6FD2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6FC2B81EC0400C3CA2D /* BacktraceReporter.swift */; }; + 6167E6FE2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6FC2B81EC0400C3CA2D /* BacktraceReporter.swift */; }; + 6167E7002B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6FF2B81EF7500C3CA2D /* BacktraceReportingFeature.swift */; }; + 6167E7012B81EF7500C3CA2D /* BacktraceReportingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E6FF2B81EF7500C3CA2D /* BacktraceReportingFeature.swift */; }; + 6167E7032B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7022B81F2EB00C3CA2D /* BacktraceReporter.swift */; }; + 6167E7042B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7022B81F2EB00C3CA2D /* BacktraceReporter.swift */; }; + 6167E7062B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7052B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift */; }; + 6167E7072B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7052B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift */; }; + 6167E70E2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E70D2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift */; }; + 6167E70F2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E70D2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift */; }; + 6167E7142B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7112B837F0B00C3CA2D /* BacktraceReportingMocks.swift */; }; + 6167E7152B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7112B837F0B00C3CA2D /* BacktraceReportingMocks.swift */; }; + 6167E71B2B837F7A00C3CA2D /* BacktraceReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7182B837F7A00C3CA2D /* BacktraceReportMocks.swift */; }; + 6167E71C2B837F7A00C3CA2D /* BacktraceReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7182B837F7A00C3CA2D /* BacktraceReportMocks.swift */; }; + 6167E7202B837FB200C3CA2D /* DDThreadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E71D2B837FB200C3CA2D /* DDThreadMocks.swift */; }; + 6167E7212B837FB200C3CA2D /* DDThreadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E71D2B837FB200C3CA2D /* DDThreadMocks.swift */; }; + 6167E7252B837FF100C3CA2D /* BinaryImageMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */; }; + 6167E7262B837FF100C3CA2D /* BinaryImageMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */; }; + 6167E7292B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */; }; + 6167E72A2B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */; }; + 6167E72C2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; + 6167E72D2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; + 616B668E259CC28E00968EE8 /* DDRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */; }; + 616F8C272BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; + 616F8C282BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; + 6170DC1C25C18729003AED5C /* PLCrashReporterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6170DC1B25C18729003AED5C /* PLCrashReporterPlugin.swift */; }; + 6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6172472625D673D7007085B3 /* CrashContextTests.swift */; }; + 617247AF25DA9BEA007085B3 /* CrashReportingObjcHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */; }; + 617247B825DAB0E2007085B3 /* DDCrashReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */; }; + 6174D6042BFB9AB600EC7469 /* WebViewTracking+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */; }; + 6174D6062BFB9D6400EC7469 /* DDWebViewTracking+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */; }; + 6174D60C2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */; }; + 6174D60D2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */; }; + 6174D6102BFDEA4600EC7469 /* SessionEndedMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60F2BFDEA4600EC7469 /* SessionEndedMetric.swift */; }; + 6174D6112BFDEA4600EC7469 /* SessionEndedMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D60F2BFDEA4600EC7469 /* SessionEndedMetric.swift */; }; + 6174D6132BFDF16C00EC7469 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6122BFDF16C00EC7469 /* BundleType.swift */; }; + 6174D6142BFDF16C00EC7469 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6122BFDF16C00EC7469 /* BundleType.swift */; }; + 6174D6162BFDF29B00EC7469 /* BundleTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6152BFDF29B00EC7469 /* BundleTypeTests.swift */; }; + 6174D6172BFDF29B00EC7469 /* BundleTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6152BFDF29B00EC7469 /* BundleTypeTests.swift */; }; + 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6192BFE449300EC7469 /* SessionEndedMetricTests.swift */; }; + 6174D61B2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D6192BFE449300EC7469 /* SessionEndedMetricTests.swift */; }; + 6174D61D2C007B3300EC7469 /* ModuleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61C2C007B3300EC7469 /* ModuleName.swift */; }; + 6174D61E2C007B3300EC7469 /* ModuleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61C2C007B3300EC7469 /* ModuleName.swift */; }; + 6174D6202C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61F2C009C6300EC7469 /* SessionEndedMetricController.swift */; }; + 6174D6212C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6174D61F2C009C6300EC7469 /* SessionEndedMetricController.swift */; }; + 6175922B2A6FA8EE0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; + 6175922D2A6FADDD0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; + 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; + 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; + 617699182A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; + 617699192A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; + 6176991B2A86121B0030022B /* HTTPClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991A2A86121B0030022B /* HTTPClientMock.swift */; }; + 6176991C2A86121B0030022B /* HTTPClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991A2A86121B0030022B /* HTTPClientMock.swift */; }; + 6176991E2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */; }; + 6176991F2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */; }; + 617699212A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699202A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift */; }; + 6176C1722ABDBA2E00131A70 /* MonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */; }; + 6176C1732ABDBA2E00131A70 /* MonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */; }; + 61776CED273BEA5500F93802 /* DebugRUMSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */; }; + 61776D4E273E6D9F00F93802 /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61776D4D273E6D9F00F93802 /* SwiftUI.swift */; }; + 6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */; }; + 6179DB572B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */; }; + 6179FFD3254ADB1700556A0B /* ObjcAppLaunchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */; }; + 6179FFDE254ADBEF00556A0B /* ObjcAppLaunchHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */; }; + 617B954224BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B954124BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift */; }; + 618236892710560900125326 /* DebugWebviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618236882710560900125326 /* DebugWebviewViewController.swift */; }; + 618353BC2A69470A0085F84A /* CoreMetricsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618353BB2A69470A0085F84A /* CoreMetricsIntegrationTests.swift */; }; + 618353BD2A69470A0085F84A /* CoreMetricsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618353BB2A69470A0085F84A /* CoreMetricsIntegrationTests.swift */; }; + 6184751526EFCF1300C7C9C5 /* DatadogTestsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6184751426EFCF1300C7C9C5 /* DatadogTestsObserver.swift */; }; + 6184751826EFD03400C7C9C5 /* DatadogTestsObserverLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 6184751726EFD03400C7C9C5 /* DatadogTestsObserverLoader.m */; }; + 6185F4AE26FE1956001A7641 /* SpanE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6185F4AD26FE1956001A7641 /* SpanE2ETests.swift */; }; + 6187A53926FCBE240015D94A /* TracerE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6187A53826FCBE240015D94A /* TracerE2ETests.swift */; }; + 6188697C2A4376F700E8996B /* RUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */; }; + 6188697D2A4376F700E8996B /* RUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */; }; + 6188900F2AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */; }; + 618890102AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */; }; + 618C0FC02B482F6800266B38 /* SpanWriteContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */; }; + 618C0FC12B482F6800266B38 /* SpanWriteContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */; }; 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C365E248E85B400520CDE /* DateFormattingTests.swift */; }; - 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */; }; - 61AD4E3824531500006E34EA /* DataFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3724531500006E34EA /* DataFormat.swift */; }; - 61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */; }; - 61B558CF2469561C001460D3 /* LoggerBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */; }; - 61B558D42469CDD8001460D3 /* TracingUUIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */; }; - 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */; }; - 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */; }; - 61B9ED1F2461E57700C0DCFF /* UITestsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */; }; - 61B9ED212462089600C0DCFF /* TracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */; }; + 618F9843265BC486009959F8 /* E2EInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F9842265BC486009959F8 /* E2EInstrumentationTests.swift */; }; + 618F984E265BC905009959F8 /* E2EConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F984D265BC905009959F8 /* E2EConfig.swift */; }; + 618F984F265BC905009959F8 /* E2EConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618F984D265BC905009959F8 /* E2EConfig.swift */; }; + 6194B92A2BB4116A00179430 /* RUMDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B9292BB4116A00179430 /* RUMDataStore.swift */; }; + 6194B92B2BB4116A00179430 /* RUMDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B9292BB4116A00179430 /* RUMDataStore.swift */; }; + 6194B92D2BB43F9C00179430 /* FatalErrorContextNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B92C2BB43F9C00179430 /* FatalErrorContextNotifier.swift */; }; + 6194B92E2BB43F9C00179430 /* FatalErrorContextNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B92C2BB43F9C00179430 /* FatalErrorContextNotifier.swift */; }; + 6194B9302BB451C100179430 /* NonFatalAppHangsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B92F2BB451C100179430 /* NonFatalAppHangsHandler.swift */; }; + 6194B9312BB451C100179430 /* NonFatalAppHangsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B92F2BB451C100179430 /* NonFatalAppHangsHandler.swift */; }; + 6194B9332BB451DB00179430 /* FatalAppHangsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B9322BB451DB00179430 /* FatalAppHangsHandler.swift */; }; + 6194B9342BB451DB00179430 /* FatalAppHangsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194B9322BB451DB00179430 /* FatalAppHangsHandler.swift */; }; + 6199362E265BA959009D7EA8 /* E2EAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6199362D265BA959009D7EA8 /* E2EAppDelegate.swift */; }; + 61993637265BA95A009D7EA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61993636265BA95A009D7EA8 /* Assets.xcassets */; }; + 6199363A265BA95A009D7EA8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61993638265BA95A009D7EA8 /* LaunchScreen.storyboard */; }; + 61993657265BB6A6009D7EA8 /* DatadogCore.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 6199365B265BB6A6009D7EA8 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 61993668265BBEDC009D7EA8 /* E2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61993667265BBEDC009D7EA8 /* E2ETests.swift */; }; + 619A29F326E64910007D62A3 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + 619CE75E2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */; }; + 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */; }; + 619CE7612A458D66005588CB /* TraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE7602A458D66005588CB /* TraceTests.swift */; }; + 619CE7622A458D66005588CB /* TraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619CE7602A458D66005588CB /* TraceTests.swift */; }; + 619F5CEC2BF5089C004BFE70 /* GlobalRUMAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */; }; + 619F5CED2BF508A4004BFE70 /* GlobalRUMAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */; }; + 61A1A44929643254007909E7 /* DatadogCoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */; }; + 61A1A44A29643254007909E7 /* DatadogCoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */; }; + 61A2CC212A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */; }; + 61A2CC222A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */; }; + 61A2CC242A44454D0000FF25 /* DDRUMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC232A44454D0000FF25 /* DDRUMTests.swift */; }; + 61A2CC252A44454D0000FF25 /* DDRUMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC232A44454D0000FF25 /* DDRUMTests.swift */; }; + 61A2CC262A4449210000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + 61A2CC2B2A4449300000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; + 61A2CC302A4449CB0000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + 61A2CC312A4449D70000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; + 61A2CC332A44A5F60000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + 61A2CC342A44A6030000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; }; + 61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */; }; + 61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */; }; + 61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC382A44B0EA0000FF25 /* Trace.swift */; }; + 61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC382A44B0EA0000FF25 /* Trace.swift */; }; + 61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC3B2A44BED30000FF25 /* Tracer.swift */; }; + 61A2CC3D2A44BED30000FF25 /* Tracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC3B2A44BED30000FF25 /* Tracer.swift */; }; + 61A763DC252DB2B3005A23F2 /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */; }; + 61AE74142AD6EF55008DB9BB /* JSONObjectMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13D22AAA20660086B5D1 /* JSONObjectMatcher.swift */; }; + 61AE74152AD6EF55008DB9BB /* JSONObjectMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612C13D22AAA20660086B5D1 /* JSONObjectMatcher.swift */; }; + 61AE74172AD7DA9B008DB9BB /* FeatureMessageMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE74162AD7DA9B008DB9BB /* FeatureMessageMocks.swift */; }; + 61AE74182AD7DA9B008DB9BB /* FeatureMessageMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE74162AD7DA9B008DB9BB /* FeatureMessageMocks.swift */; }; + 61B3BD52266128D300A9BEF0 /* LoggerE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B3BD51266128D300A9BEF0 /* LoggerE2ETests.swift */; }; + 61B5E42126DF85C7000B0A5F /* DDRUMMonitor+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42026DF85C7000B0A5F /* DDRUMMonitor+apiTests.m */; }; + 61B5E42726DFB145000B0A5F /* DDDatadog+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42626DFB145000B0A5F /* DDDatadog+apiTests.m */; }; + 61B5E42926DFB60A000B0A5F /* DDConfiguration+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42826DFB60A000B0A5F /* DDConfiguration+apiTests.m */; }; + 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42A26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m */; }; + 61B7885D25C180CB002675B5 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; + 61B7886225C180CB002675B5 /* CrashReportingPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B7886125C180CB002675B5 /* CrashReportingPluginTests.swift */; }; + 61B7886425C180CB002675B5 /* DatadogCrashReporting.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B7885625C180CB002675B5 /* DatadogCrashReporting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 61B8BA91281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B8BA90281812F60068AFF4 /* KronosInternetAddressTests.swift */; }; + 61B8BA92281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B8BA90281812F60068AFF4 /* KronosInternetAddressTests.swift */; }; + 61BAD46A26415FCE001886CA /* OTSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BAD46926415FCE001886CA /* OTSpanTests.swift */; }; 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 */; }; - 61C36470243B5C8300C4D4E6 /* ServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */; }; - 61C5A88424509A0C00DA608C /* DDSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87824509A0C00DA608C /* DDSpan.swift */; }; - 61C5A88524509A0C00DA608C /* DDNoOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87924509A0C00DA608C /* DDNoOps.swift */; }; - 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */; }; - 61C5A88724509A0C00DA608C /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87C24509A0C00DA608C /* Casting.swift */; }; - 61C5A88824509A0C00DA608C /* Warnings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87D24509A0C00DA608C /* Warnings.swift */; }; - 61C5A88924509A0C00DA608C /* DDSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */; }; - 61C5A88A24509A0C00DA608C /* SpanFileOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */; }; - 61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88124509A0C00DA608C /* SpanOutput.swift */; }; - 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */; }; - 61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88D24509A1F00DA608C /* Tracer.swift */; }; - 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88F24509AA700DA608C /* TracingFeature.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 */; }; - 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89824509C1100DA608C /* DDSpanTests.swift */; }; - 61C5A89E24509C1100DA608C /* WarningsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89A24509C1100DA608C /* WarningsTests.swift */; }; - 61C5A89F24509C1100DA608C /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89B24509C1100DA608C /* UUID.swift */; }; - 61C5A8A024509C1100DA608C /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89C24509C1100DA608C /* Casting.swift */; }; - 61C5A8A624509FAA00DA608C /* SpanEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */; }; - 61C5A8A724509FAA00DA608C /* SpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */; }; - 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D447E124917F8F00649287 /* DateFormatting.swift */; }; - 61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */; }; - 61E45BD22450F65B00F2C652 /* SpanBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */; }; - 61E45BE5245196EA00F2C652 /* SpanFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */; }; + 61C713A32A3B78F900FA735A /* RUMMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A02A3B78F900FA735A /* RUMMonitorProtocol.swift */; }; + 61C713A42A3B78F900FA735A /* RUMMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A02A3B78F900FA735A /* RUMMonitorProtocol.swift */; }; + 61C713A52A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A12A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift */; }; + 61C713A62A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A12A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift */; }; + 61C713A72A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A22A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift */; }; + 61C713A82A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A22A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift */; }; + 61C713AA2A3B790B00FA735A /* Monitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A92A3B790B00FA735A /* Monitor.swift */; }; + 61C713AB2A3B790B00FA735A /* Monitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713A92A3B790B00FA735A /* Monitor.swift */; }; + 61C713AD2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713AC2A3B793E00FA735A /* RUMMonitorProtocolTests.swift */; }; + 61C713AE2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713AC2A3B793E00FA735A /* RUMMonitorProtocolTests.swift */; }; + 61C713B32A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B22A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift */; }; + 61C713B42A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B22A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift */; }; + 61C713B62A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B52A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift */; }; + 61C713B72A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B52A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift */; }; + 61C713B92A3C935C00FA735A /* RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B82A3C935C00FA735A /* RUM.swift */; }; + 61C713BA2A3C935C00FA735A /* RUM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713B82A3C935C00FA735A /* RUM.swift */; }; + 61C713BC2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */; }; + 61C713BD2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */; }; + 61C713C02A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713BF2A3C9DAD00FA735A /* RequestBuilderTests.swift */; }; + 61C713C12A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713BF2A3C9DAD00FA735A /* RequestBuilderTests.swift */; }; + 61C713CA2A3DC22700FA735A /* RUMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713C92A3DC22700FA735A /* RUMTests.swift */; }; + 61C713CB2A3DC22700FA735A /* RUMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713C92A3DC22700FA735A /* RUMTests.swift */; }; + 61C713D02A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */; }; + 61C713D12A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */; }; + 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; + 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */; }; + 61CE2E5F2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */; }; + 61CE2E602BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */; }; + 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; + 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CE58592B48174D00479510 /* SpanWriteContext.swift */; }; + 61D03BE0273404E700367DE0 /* RUMDataModels+objcTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */; }; + 61D3E0D2277B23F1008BE766 /* KronosInternetAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */; }; + 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */; }; + 61D3E0D4277B23F1008BE766 /* KronosTimeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */; }; + 61D3E0D5277B23F1008BE766 /* KronosNTPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */; }; + 61D3E0D6277B23F1008BE766 /* KronosClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CC277B23F0008BE766 /* KronosClock.swift */; }; + 61D3E0D7277B23F1008BE766 /* KronosData+Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */; }; + 61D3E0D8277B23F1008BE766 /* KronosNTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */; }; + 61D3E0D9277B23F1008BE766 /* KronosNTPProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */; }; + 61D3E0DA277B23F1008BE766 /* KronosTimeFreeze.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */; }; + 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */; }; + 61D3E0E4277B3D92008BE766 /* KronosNTPPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */; }; + 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */; }; + 61D3E0EA277E0C58008BE766 /* KronosE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */; }; + 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */; }; + 61DA6F6C2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */; }; + 61DA6F6D2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */; }; + 61DA8CA928609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; + 61DA8CAA28609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; + 61DA8CAC2861C3720074A606 /* DirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */; }; + 61DA8CAD2861C3720074A606 /* DirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */; }; + 61DA8CAF28620C760074A606 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CAE28620C760074A606 /* Cryptography.swift */; }; + 61DA8CB028620C760074A606 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CAE28620C760074A606 /* Cryptography.swift */; }; + 61DA8CB2286215DE0074A606 /* CryptographyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CB1286215DE0074A606 /* CryptographyTests.swift */; }; + 61DA8CB3286215DE0074A606 /* CryptographyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CB1286215DE0074A606 /* CryptographyTests.swift */; }; + 61DA8CB828647A500074A606 /* InternalLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CB728647A500074A606 /* InternalLoggerTests.swift */; }; + 61DA8CB928647A500074A606 /* InternalLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CB728647A500074A606 /* InternalLoggerTests.swift */; }; + 61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */; }; + 61DCC8472C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */; }; + 61DCC8482C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */; }; + 61DCC84A2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */; }; + 61DCC84B2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */; }; + 61DCC84E2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC84D2C071DCD00CB59E5 /* TelemetryInterceptor.swift */; }; + 61DCC84F2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCC84D2C071DCD00CB59E5 /* TelemetryInterceptor.swift */; }; 61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; }; 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; }; - 61E909ED24A24DD3005EA2DE /* OTSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E624A24DD3005EA2DE /* OTSpan.swift */; }; - 61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E724A24DD3005EA2DE /* OTFormat.swift */; }; - 61E909EF24A24DD3005EA2DE /* OTGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E824A24DD3005EA2DE /* OTGlobal.swift */; }; - 61E909F024A24DD3005EA2DE /* OTTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E924A24DD3005EA2DE /* OTTracer.swift */; }; - 61E909F124A24DD3005EA2DE /* OTReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EA24A24DD3005EA2DE /* OTReference.swift */; }; - 61E909F224A24DD3005EA2DE /* OTConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EB24A24DD3005EA2DE /* OTConstants.swift */; }; - 61E909F324A24DD3005EA2DE /* OTSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */; }; - 61E909F624A32D1C005EA2DE /* OTGlobalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */; }; - 61E917CF2464270500E6C631 /* EncodableValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917CE2464270500E6C631 /* EncodableValueTests.swift */; }; - 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D02465423600E6C631 /* TracerConfiguration.swift */; }; - 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */; }; + 61E5333824B84EE2003D6C4E /* DebugRUMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333724B84EE2003D6C4E /* DebugRUMViewController.swift */; }; + 61E8C5082B28898800E709B4 /* StartingRUMSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */; }; + 61E8C5092B28898800E709B4 /* StartingRUMSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */; }; + 61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */; }; + 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */; }; + 61EF78C1257F842000EDCCB3 /* FeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EF78C0257F842000EDCCB3 /* FeatureTests.swift */; }; 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A6192498A51700075390 /* CoreMocks.swift */; }; - 61F1A621249A45E400075390 /* DDSpanContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A620249A45E400075390 /* DDSpanContextTests.swift */; }; - 61F1A623249B811200075390 /* Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A622249B811200075390 /* Encoding.swift */; }; - 61F8CC092469295500FE2908 /* DatadogConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */; }; - 61FB222D244A21ED00902D19 /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; - 61FB2230244E1BE900902D19 /* LoggingFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */; }; - 9E330A8E24ADE1250031408E /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */; }; - 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; - 9E493E1C249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */; }; - 9E544A4D24752A8900E83072 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */; }; - 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */; }; - 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */; }; - 9E58E8E124615C75008E5063 /* JSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E024615C75008E5063 /* JSONEncoder.swift */; }; + 61F2723F25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */; }; + 61F2724925C943C500D54BF8 /* CrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2724825C943C500D54BF8 /* CrashReporterTests.swift */; }; + 61F2727425C9509D00D54BF8 /* ThirdPartyCrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2727325C9509D00D54BF8 /* ThirdPartyCrashReporter.swift */; }; + 61F2728B25C9561A00D54BF8 /* PLCrashReporterIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2728A25C9561A00D54BF8 /* PLCrashReporterIntegration.swift */; }; + 61F272B125C95ED800D54BF8 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2729A25C95EB200D54BF8 /* Mocks.swift */; }; + 61F3E3632BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */; }; + 61F3E3642BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */; }; + 61F3E3662BC595F600C7881E /* HTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */; }; + 61F3E3672BC595F600C7881E /* HTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */; }; + 61F3E36D2BC7D66700C7881E /* HeadBasedSamplingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E36C2BC7D66700C7881E /* HeadBasedSamplingTests.swift */; }; + 61F3E36E2BC7D66700C7881E /* HeadBasedSamplingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3E36C2BC7D66700C7881E /* HeadBasedSamplingTests.swift */; }; + 61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */; }; + 61F930BE2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */; }; + 61F930BF2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */; }; + 61F930C22BA1C41A005F0EE2 /* TLVBlockReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */; }; + 61F930C32BA1C41A005F0EE2 /* TLVBlockReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */; }; + 61F930C52BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */; }; + 61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */; }; + 61F930C82BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */; }; + 61F930C92BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */; }; + 61F930CB2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; }; + 61F930CC2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; }; + 61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */; }; + 61FC5F3525CC1898006BB4DE /* CrashContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FC5F3425CC1898006BB4DE /* CrashContextProviderTests.swift */; }; + 61FDBA1326971953001D9D43 /* CrashReportMinifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA1226971953001D9D43 /* CrashReportMinifier.swift */; }; + 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 */; }; + 9E5B6D30270C85AB002499B8 /* RUMUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5B6D2F270C85AB002499B8 /* RUMUtils.swift */; }; + 9E5B6D32270DE9E5002499B8 /* RUMTrackingConsentE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5B6D31270DE9E5002499B8 /* RUMTrackingConsentE2ETests.swift */; }; + 9E5BD8042819742200CB568E /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B03ECC274FF00E00EB1AE1 /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 9E5BD8062819742C00CB568E /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E5BD8052819742C00CB568E /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 9E64849D27031071007CCD7B /* RUMGlobalE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E64849C27031071007CCD7B /* RUMGlobalE2ETests.swift */; }; 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; - 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 9EB47B92247443FA004F90BE /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */; }; - 9ED583A32498C222004CFF2A /* TracingAutoInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */; }; + 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9EE5AD8226205B82001E699E /* DDNSURLSessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */; }; + A70A82652A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */; }; + 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 */; }; + 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 */; }; + A728ADB02934EB0900397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; + A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */; }; + A73A54982B16406900E1F7E3 /* ResourcesFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A54972B16406900E1F7E3 /* ResourcesFeature.swift */; }; + A74A72812B0CEE4900771FEB /* ResourceRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74A72802B0CEE4900771FEB /* ResourceRequestBuilder.swift */; }; + A74A72852B10CC6700771FEB /* ResourceRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74A72842B10CC6700771FEB /* ResourceRequestBuilderTests.swift */; }; + A74A72872B10CE4100771FEB /* ResourceMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74A72862B10CE4100771FEB /* ResourceMocks.swift */; }; + A74A72892B10D95D00771FEB /* MultipartBuilderSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */; }; + A795069C2B974C8200AC4814 /* SessionReplay+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A795069B2B974C8100AC4814 /* SessionReplay+objc.swift */; }; + A79B0F64292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */; }; + A79B0F65292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */; }; + A79B0F66292BD7CA008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */; }; + A79B0F67292BD7CC008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */; }; + A7B932F52B1F694000AE6477 /* ResourcesProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */; }; + A7B932FB2B1F6A0A00AE6477 /* EnrichedRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */; }; + A7B932FC2B1F6A0A00AE6477 /* SRDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */; }; + A7B932FD2B1F6A0A00AE6477 /* EnrichedResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */; }; + A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */; }; + A7CA21802BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */; }; + A7CA21812BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */; }; + A7CA21832BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */; }; + A7CA21842BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */; }; + A7D9528A2B28BD94004C79B1 /* ResourceProcessorSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */; }; + A7D9528C2B28C18D004C79B1 /* ResourceProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */; }; + A7DA18042AB0C91200F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; + A7DA18052AB0C91300F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */; }; + A7DA18072AB0CA5E00F76337 /* DDUIKitRUMActionsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */; }; + A7EA11622AB0CE6C00C73970 /* DDUIKitRUMActionsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */; }; + A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */; }; + 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 */; }; + D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A5287476230047275C /* ServerOffsetPublisher.swift */; }; + D20605A7287476230047275C /* ServerOffsetPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A5287476230047275C /* ServerOffsetPublisher.swift */; }; + D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */; }; + D20605AA2874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */; }; + D20605B22874E1660047275C /* CarrierInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B12874E1660047275C /* CarrierInfoPublisher.swift */; }; + D20605B32874E1660047275C /* CarrierInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B12874E1660047275C /* CarrierInfoPublisher.swift */; }; + D20605B6287572640047275C /* DatadogContextProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B5287572640047275C /* DatadogContextProviderMock.swift */; }; + D20605B7287572640047275C /* DatadogContextProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B5287572640047275C /* DatadogContextProviderMock.swift */; }; + D20605B92875729E0047275C /* ContextValuePublisherMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605B82875729E0047275C /* ContextValuePublisherMock.swift */; }; + 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 */; }; + 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; }; + D207319529A522F600ECBF94 /* LogsFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F1FAF283E227100651A3A /* LogsFeature.swift */; }; + D207319629A522F600ECBF94 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194E4BB2878AF7600EB6307 /* ConsoleLogger.swift */; }; + D207319729A5232A00ECBF94 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D20731A929A5279D00ECBF94 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194E4BB2878AF7600EB6307 /* ConsoleLogger.swift */; }; + D20731AB29A5279D00ECBF94 /* LogsFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F1FAF283E227100651A3A /* LogsFeature.swift */; }; + D20731B529A528DA00ECBF94 /* LogEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC32423979B00786299 /* LogEventBuilder.swift */; }; + D20731B629A528DA00ECBF94 /* LogEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC32423979B00786299 /* LogEventBuilder.swift */; }; + D20731C229A528EB00ECBF94 /* LogEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC22423979B00786299 /* LogEventEncoder.swift */; }; + D20731C329A528EB00ECBF94 /* LogEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C1F5B271484B400922024 /* LogEventMapper.swift */; }; + D20731C529A528EC00ECBF94 /* LogEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC42423979B00786299 /* LogEventSanitizer.swift */; }; + D20731C729A528ED00ECBF94 /* LogEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC22423979B00786299 /* LogEventEncoder.swift */; }; + D20731C829A528ED00ECBF94 /* LogEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C1F5B271484B400922024 /* LogEventMapper.swift */; }; + D20731CA29A528ED00ECBF94 /* LogEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BC42423979B00786299 /* LogEventSanitizer.swift */; }; + D20731CB29A52E6000ECBF94 /* Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B8F2768FDDE00870CBF /* Sampler.swift */; }; + D20731CC29A52E6000ECBF94 /* Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B8F2768FDDE00870CBF /* Sampler.swift */; }; + D20731CD29A52E8700ECBF94 /* SamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B912768FF3100870CBF /* SamplerTests.swift */; }; + D20731CE29A52E8700ECBF94 /* SamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613C6B912768FF3100870CBF /* SamplerTests.swift */; }; + D20FD9CF2AC6FF42004D3569 /* WebViewLogReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FD9CE2AC6FF42004D3569 /* WebViewLogReceiverTests.swift */; }; + D20FD9D02AC6FF42004D3569 /* WebViewLogReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FD9CE2AC6FF42004D3569 /* WebViewLogReceiverTests.swift */; }; + 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 */; }; + D214DA8429DF2D5E004D0AE8 /* CrashReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6161247825CA9CA6009901BE /* CrashReporting.swift */; }; + D214DA8929DF2D6A004D0AE8 /* CrashReportSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6112B11325C84E7900B37771 /* CrashReportSender.swift */; }; + D214DA8A29DF2D6A004D0AE8 /* CrashContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6161249D25CAB340009901BE /* CrashContext.swift */; }; + D214DA8B29DF2D6A004D0AE8 /* CrashContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616124A625CAC268009901BE /* CrashContextProvider.swift */; }; + D214DA8C29DF2D6B004D0AE8 /* CrashReportSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6112B11325C84E7900B37771 /* CrashReportSender.swift */; }; + D214DA8D29DF2D6B004D0AE8 /* CrashContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6161249D25CAB340009901BE /* CrashContext.swift */; }; + D214DA8E29DF2D6B004D0AE8 /* CrashContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616124A625CAC268009901BE /* CrashContextProvider.swift */; }; + D214DA9429DF2F5E004D0AE8 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D214DA9929DF2F6A004D0AE8 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D2160C9A29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */; }; + D2160C9B29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */; }; + D2160C9E29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9629C0DE5600FAA9A5 /* TracingHeaderType.swift */; }; + D2160C9F29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9629C0DE5600FAA9A5 /* TracingHeaderType.swift */; }; + D2160CA029C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9729C0DE5700FAA9A5 /* HostsSanitizer.swift */; }; + D2160CA129C0DE5700FAA9A5 /* HostsSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9729C0DE5700FAA9A5 /* HostsSanitizer.swift */; }; + D2160CA229C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */; }; + D2160CA329C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */; }; + D2160CC529C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */; }; + D2160CC629C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */; }; + D2160CC929C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */; }; + D2160CCA29C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */; }; + D2160CD429C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */; }; + D2160CD529C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */; }; + D2160CD829C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */; }; + D2160CD929C0DF6700FAA9A5 /* FirstPartyHostsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */; }; + D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */; }; + D2160CDD29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */; }; + D2160CDE29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */; }; + D2160CDF29C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */; }; + D2160CE029C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */; }; + D2160CE129C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */; }; + D2160CE429C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CE329C0DFED00FAA9A5 /* MethodSwizzler.swift */; }; + D2160CE629C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CE329C0DFED00FAA9A5 /* MethodSwizzler.swift */; }; + D2160CE929C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CE829C0E00200FAA9A5 /* MethodSwizzlerTests.swift */; }; + D2160CEA29C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CE829C0E00200FAA9A5 /* MethodSwizzlerTests.swift */; }; + D2160CED29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */; }; + D2160CEE29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */; }; + D2160CF029C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CEF29C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift */; }; + D2160CF129C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CEF29C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift */; }; + D2160CF229C0ED3C00FAA9A5 /* ServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */; }; + D2160CF329C0ED3C00FAA9A5 /* ServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */; }; + D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49B52889416300802B2D /* UploadPerformancePreset.swift */; }; + D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49B52889416300802B2D /* UploadPerformancePreset.swift */; }; + D2160CF729C0EE2B00FAA9A5 /* UploadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */; }; + D2160CF829C0EE2B00FAA9A5 /* UploadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */; }; + D2181A8E2B051B7900A518C0 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2181A8D2B051B7900A518C0 /* URLSessionSwizzlerTests.swift */; }; + 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 */; }; + D21AE6BD29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21AE6BB29E5EDAF0064BF29 /* TelemetryTests.swift */; }; + D21C26C528A3B49C005DD405 /* FeatureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26C428A3B49C005DD405 /* FeatureStorage.swift */; }; + D21C26C628A3B49C005DD405 /* FeatureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26C428A3B49C005DD405 /* FeatureStorage.swift */; }; + D21C26D128A64599005DD405 /* MessageBusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26D028A64599005DD405 /* MessageBusTests.swift */; }; + D21C26D228A64599005DD405 /* MessageBusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26D028A64599005DD405 /* MessageBusTests.swift */; }; + D2216EC02A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2216EBF2A94DE2800ADAEC8 /* FeatureBaggage.swift */; }; + 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 */; }; + D224430729E95C2E00274EC7 /* MessageBus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D214DAA429E072D7004D0AE8 /* MessageBus.swift */; }; + D224430D29E95D6700274EC7 /* CrashReportReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */; }; + D224430E29E95D6700274EC7 /* CrashReportReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */; }; + D224430F29E9779F00274EC7 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D248ED4728081B9B00B315B4 /* TelemetryReceiverTests.swift */; }; + D224431029E977A100274EC7 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D248ED4728081B9B00B315B4 /* TelemetryReceiverTests.swift */; }; + D22743D829DEB8B4001A7EF9 /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; }; + D22743D929DEB8B4001A7EF9 /* VitalInfoSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */; }; + D22743DA29DEB8B4001A7EF9 /* VitalMemoryReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */; }; + D22743DB29DEB8B4001A7EF9 /* VitalCPUReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */; }; + D22743DC29DEB8B4001A7EF9 /* VitalRefreshRateReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */; }; + D22743DD29DEB8B5001A7EF9 /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; }; + D22743DE29DEB8B5001A7EF9 /* VitalInfoSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */; }; + D22743DF29DEB8B5001A7EF9 /* VitalMemoryReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */; }; + D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */; }; + D22743E129DEB8B5001A7EF9 /* VitalRefreshRateReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */; }; + D22743E229DEB90B001A7EF9 /* RUMDebuggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */; }; + D22743E329DEB90B001A7EF9 /* RUMDebuggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */; }; + D22743E429DEB933001A7EF9 /* UIViewControllerSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */; }; + D22743E529DEB934001A7EF9 /* UIViewControllerSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */; }; + D22743E629DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */; }; + D22743E729DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */; }; + D22743E929DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */; }; + 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 */; }; + D22C5BCC2A98A5400024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BCA2A98A5400024CC1F /* Baggages.swift */; }; + D22C5BD02A98A6660024CC1F /* Baggages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22C5BCD2A98A65D0024CC1F /* Baggages.swift */; }; + D22F06D729DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; + D22F06D829DAFD500026CC3C /* FixedWidthInteger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */; }; + D22F06D929DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */; }; + D22F06DA29DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */; }; + D2303996298D50F1001A1FA3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; + D2303997298D50F1001A1FA3 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D2303998298D50F1001A1FA3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; + D2303999298D50F1001A1FA3 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D230399A298D50F1001A1FA3 /* XCTest.framework in Headers */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; + D230399B298D50F1001A1FA3 /* DatadogCore.framework in Headers */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D230399C298D50F1001A1FA3 /* XCTest.framework in Headers */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; + D230399D298D50F1001A1FA3 /* DatadogCore.framework in Headers */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D230399E298D50F1001A1FA3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579593298ABCF5008A1BE5 /* XCTest.framework */; }; + D23039DD298D5235001A1FA3 /* DD.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039AD298D5234001A1FA3 /* DD.swift */; }; + D23039DE298D5235001A1FA3 /* Writer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039AF298D5235001A1FA3 /* Writer.swift */; }; + D23039E0298D5235001A1FA3 /* DatadogCoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */; }; + D23039E1298D5236001A1FA3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B3298D5235001A1FA3 /* AppState.swift */; }; + D23039E2298D5236001A1FA3 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B4298D5235001A1FA3 /* UserInfo.swift */; }; + D23039E3298D5236001A1FA3 /* BatteryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B5298D5235001A1FA3 /* BatteryStatus.swift */; }; + D23039E4298D5236001A1FA3 /* CarrierInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B6298D5235001A1FA3 /* CarrierInfo.swift */; }; + D23039E5298D5236001A1FA3 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B7298D5235001A1FA3 /* DateProvider.swift */; }; + D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B8298D5235001A1FA3 /* Sysctl.swift */; }; + D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B9298D5235001A1FA3 /* NetworkConnectionInfo.swift */; }; + D23039E8298D5236001A1FA3 /* DatadogContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BA298D5235001A1FA3 /* DatadogContext.swift */; }; + D23039E9298D5236001A1FA3 /* TrackingConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BB298D5235001A1FA3 /* TrackingConsent.swift */; }; + D23039EA298D5236001A1FA3 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BC298D5235001A1FA3 /* DeviceInfo.swift */; }; + D23039EB298D5236001A1FA3 /* DatadogFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BD298D5235001A1FA3 /* DatadogFeature.swift */; }; + D23039EC298D5236001A1FA3 /* LaunchTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BE298D5235001A1FA3 /* LaunchTime.swift */; }; + D23039EE298D5236001A1FA3 /* FeatureMessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C1298D5235001A1FA3 /* FeatureMessageReceiver.swift */; }; + D23039EF298D5236001A1FA3 /* FeatureMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C2298D5235001A1FA3 /* FeatureMessage.swift */; }; + D23039F0298D5236001A1FA3 /* AnyEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C4298D5235001A1FA3 /* AnyEncoder.swift */; }; + D23039F1298D5236001A1FA3 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C5298D5235001A1FA3 /* AnyDecodable.swift */; }; + D23039F2298D5236001A1FA3 /* AnyDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C6298D5235001A1FA3 /* AnyDecoder.swift */; }; + D23039F3298D5236001A1FA3 /* DynamicCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C7298D5235001A1FA3 /* DynamicCodingKey.swift */; }; + D23039F4298D5236001A1FA3 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C8298D5235001A1FA3 /* AnyCodable.swift */; }; + D23039F5298D5236001A1FA3 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C9298D5235001A1FA3 /* AnyEncodable.swift */; }; + D23039F6298D5236001A1FA3 /* Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CB298D5235001A1FA3 /* Attributes.swift */; }; + D23039F7298D5236001A1FA3 /* AttributesSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CC298D5235001A1FA3 /* AttributesSanitizer.swift */; }; + D23039F8298D5236001A1FA3 /* InternalLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CE298D5235001A1FA3 /* InternalLogger.swift */; }; + D23039F9298D5236001A1FA3 /* CoreLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CF298D5235001A1FA3 /* CoreLogger.swift */; }; + D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D0298D5235001A1FA3 /* Telemetry.swift */; }; + D23039FB298D5236001A1FA3 /* URLRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D2298D5235001A1FA3 /* URLRequestBuilder.swift */; }; + D23039FC298D5236001A1FA3 /* DataFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D3298D5235001A1FA3 /* DataFormat.swift */; }; + D23039FD298D5236001A1FA3 /* DataCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D4298D5235001A1FA3 /* DataCompression.swift */; }; + D23039FE298D5236001A1FA3 /* FeatureRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D5298D5235001A1FA3 /* FeatureRequestBuilder.swift */; }; + D23039FF298D5236001A1FA3 /* Foundation+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */; }; + D2303A00298D5236001A1FA3 /* DatadogExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D8298D5235001A1FA3 /* DatadogExtended.swift */; }; + D2303A01298D5236001A1FA3 /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D9298D5235001A1FA3 /* DateFormatting.swift */; }; + D2303A02298D5236001A1FA3 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039DB298D5235001A1FA3 /* ReadWriteLock.swift */; }; + D2303A03298D5236001A1FA3 /* DDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039DC298D5235001A1FA3 /* DDError.swift */; }; + D2303A04298D5317001A1FA3 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D2303A0A298D5412001A1FA3 /* AsyncWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2303A09298D5412001A1FA3 /* AsyncWriter.swift */; }; + D2303A0B298D5412001A1FA3 /* AsyncWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2303A09298D5412001A1FA3 /* AsyncWriter.swift */; }; + D23354FC2A42E32000AFCAE2 /* InternalExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */; }; + D23354FD2A42E32000AFCAE2 /* InternalExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */; }; + D234613128B7713000055D4C /* FeatureContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D234613028B7712F00055D4C /* FeatureContextTests.swift */; }; + D234613228B7713000055D4C /* FeatureContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D234613028B7712F00055D4C /* FeatureContextTests.swift */; }; + D23F8E5229DDCD28001CFAE8 /* UIViewControllerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA2251118FB00C816E5 /* UIViewControllerHandler.swift */; }; + D23F8E5329DDCD28001CFAE8 /* RUMCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63A24BF1A4B008053F2 /* RUMCommand.swift */; }; + D23F8E5429DDCD28001CFAE8 /* ValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611529A425E3DD51004F740E /* ValuePublisher.swift */; }; + D23F8E5529DDCD28001CFAE8 /* RUMEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122ED325B1B84D00F9C7F5 /* RUMEventSanitizer.swift */; }; + D23F8E5729DDCD28001CFAE8 /* RUMScopeDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6122514727FDFF82004F5AE4 /* RUMScopeDependencies.swift */; }; + D23F8E5829DDCD28001CFAE8 /* VitalMemoryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */; }; + D23F8E5929DDCD28001CFAE8 /* WebViewEventReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CBC26A294383F200134409 /* WebViewEventReceiver.swift */; }; + D23F8E5A29DDCD28001CFAE8 /* RUMResourceScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB024C839460082C633 /* RUMResourceScope.swift */; }; + D23F8E5C29DDCD28001CFAE8 /* RUMApplicationScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63D24BF1B91008053F2 /* RUMApplicationScope.swift */; }; + D23F8E5D29DDCD28001CFAE8 /* SwiftUIViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */; }; + D23F8E5E29DDCD28001CFAE8 /* VitalInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */; }; + D23F8E5F29DDCD28001CFAE8 /* UIApplicationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */; }; + D23F8E6029DDCD28001CFAE8 /* PerformanceMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = E179FB4D28F80A6400CC2698 /* PerformanceMetric.swift */; }; + D23F8E6129DDCD28001CFAE8 /* RUMConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2EA29CC6D6F0063802D /* RUMConfiguration.swift */; }; + D23F8E6329DDCD28001CFAE8 /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; + D23F8E6429DDCD28001CFAE8 /* SwiftUIViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24985A12728048B00B4F72D /* SwiftUIViewHandler.swift */; }; + D23F8E6529DDCD28001CFAE8 /* RUMFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2E729CC6B680063802D /* RUMFeature.swift */; }; + D23F8E6629DDCD28001CFAE8 /* RUMDebugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B22E5924F3E6B700DC26D2 /* RUMDebugging.swift */; }; + D23F8E6729DDCD28001CFAE8 /* RUMUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFD624C7265300589570 /* RUMUUID.swift */; }; + D23F8E6829DDCD28001CFAE8 /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */; }; + D23F8E6929DDCD28001CFAE8 /* RUMContextAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CBC26D294395A300134409 /* RUMContextAttributes.swift */; }; + D23F8E6B29DDCD28001CFAE8 /* RUMMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333524B84B43003D6C4E /* RUMMonitor.swift */; }; + D23F8E6C29DDCD28001CFAE8 /* RUMContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156CB8D24DDA1B5008CB2B2 /* RUMContextProvider.swift */; }; + D23F8E6D29DDCD28001CFAE8 /* ViewIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF9A4425AC5DEA001058CC /* ViewIdentifier.swift */; }; + D23F8E6E29DDCD28001CFAE8 /* RUMViewsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */; }; + D23F8E6F29DDCD28001CFAE8 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2ED29CC73240063802D /* RequestBuilder.swift */; }; + D23F8E7029DDCD28001CFAE8 /* URLSessionRUMResourcesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB11E29D30AF000737A9A /* URLSessionRUMResourcesHandler.swift */; }; + D23F8E7129DDCD28001CFAE8 /* RUMEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF281D24B8968D000B3D9B /* RUMEventBuilder.swift */; }; + D23F8E7229DDCD28001CFAE8 /* ErrorMessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */; }; + D23F8E7329DDCD28001CFAE8 /* SwiftUIActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */; }; + D23F8E7429DDCD28001CFAE8 /* RUMCommandSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */; }; + D23F8E7529DDCD28001CFAE8 /* RUMUserActionScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */; }; + D23F8E7629DDCD28001CFAE8 /* RUMConnectivityInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B0A4E24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift */; }; + D23F8E7729DDCD28001CFAE8 /* UIKitRUMViewsPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA62512144600C816E5 /* UIKitRUMViewsPredicate.swift */; }; + D23F8E7829DDCD28001CFAE8 /* LongTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */; }; + D23F8E7A29DDCD28001CFAE8 /* SessionReplayDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950ED291C058F00470E0C /* SessionReplayDependency.swift */; }; + D23F8E7B29DDCD28001CFAE8 /* RUMDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FD9FCB28533EDF00214BD9 /* RUMDeviceInfo.swift */; }; + D23F8E7C29DDCD28001CFAE8 /* RUMOffViewEventsHandlingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */; }; + D23F8E7D29DDCD28001CFAE8 /* RUMScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63624BF191F008053F2 /* RUMScope.swift */; }; + D23F8E7E29DDCD28001CFAE8 /* CrashReportReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236BE2729520FED00676E67 /* CrashReportReceiver.swift */; }; + D23F8E7F29DDCD28001CFAE8 /* UIViewControllerSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA42511190E00C816E5 /* UIViewControllerSwizzler.swift */; }; + D23F8E8029DDCD28001CFAE8 /* VitalInfoSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */; }; + D23F8E8129DDCD28001CFAE8 /* RUMViewScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C21124C5951400C0321C /* RUMViewScope.swift */; }; + D23F8E8229DDCD28001CFAE8 /* RUMSessionScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C20624C098FC00C0321C /* RUMSessionScope.swift */; }; + D23F8E8329DDCD28001CFAE8 /* RUMUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B0A4A24EBC43D00A2A780 /* RUMUser.swift */; }; + D23F8E8429DDCD28001CFAE8 /* UIKitRUMUserActionsPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */; }; + D23F8E8529DDCD28001CFAE8 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */; }; + D23F8E8629DDCD28001CFAE8 /* RUMDataModelsMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715F824DC13A100FC0F69 /* RUMDataModelsMapping.swift */; }; + D23F8E8729DDCD28001CFAE8 /* RUMInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */; }; + D23F8E8829DDCD28001CFAE8 /* VitalCPUReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */; }; + D23F8E8929DDCD28001CFAE8 /* RUMOperatingSystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */; }; + D23F8E8A29DDCD28001CFAE8 /* RUMEventsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E81EF25A740140084B751 /* RUMEventsMapper.swift */; }; + 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 /* 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 */; }; + D23F8EA329DDCD38001CFAE8 /* RUMUserActionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */; }; + 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 */; }; + 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 */; }; + D23F8EAE29DDCD38001CFAE8 /* DDTAssertValidRUMUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FCB29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift */; }; + D23F8EAF29DDCD38001CFAE8 /* RUMScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */; }; + D23F8EB029DDCD38001CFAE8 /* SessionReplayDependencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */; }; + D23F8EB129DDCD38001CFAE8 /* RUMViewScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */; }; + D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611529AD25E3E429004F740E /* ValuePublisherTests.swift */; }; + 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 /* 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 */; }; + D23F8EBF29DDCD38001CFAE8 /* URLSessionRUMResourcesHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB12129D34A5F00737A9A /* URLSessionRUMResourcesHandlerTests.swift */; }; + D23F8EC029DDCD38001CFAE8 /* RUMEventSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */; }; + D23F8EC129DDCD38001CFAE8 /* RUMEventsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E81F625A743600084B751 /* RUMEventsMapperTests.swift */; }; + D23F8EC429DDCD38001CFAE8 /* RUMCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */; }; + D23F8EC629DDCD38001CFAE8 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D23F8EC729DDCD38001CFAE8 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + D23F8ECE29DDCD53001CFAE8 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D240680827CE6C9E00C04F44 /* ConsoleOutputInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */; }; + D240681E27CE6C9E00C04F44 /* ExampleAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */; }; + D240682B27CE6C9E00C04F44 /* UIButton+Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */; }; + D240682D27CE6C9E00C04F44 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614CADD62510BAC000B93D2D /* Environment.swift */; }; + D240683D27CE6C9E00C04F44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; + D240685527CF5D0100C04F44 /* DatadogCore.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240685927CF5D0100C04F44 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240686027CF5D0100C04F44 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240686827CF642900C04F44 /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61776D4D273E6D9F00F93802 /* SwiftUI.swift */; }; + D240687027CF971C00C04F44 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + D240687127CF971C00C04F44 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D240687227CF971C00C04F44 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */; }; + D240687327CF971C00C04F44 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */; }; + D240687827CF982B00C04F44 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + D240687B27CF982C00C04F44 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; + D240687C27CF982C00C04F44 /* DatadogCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240687D27CF982D00C04F44 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; + D240687E27CF982D00C04F44 /* DatadogCrashReporting.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240687F27CF982F00C04F44 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; + D240688027CF982F00C04F44 /* DatadogObjc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D240688627CFA64A00C04F44 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D240688427CFA64A00C04F44 /* LaunchScreen.storyboard */; }; + D242C29E2A14D6A6004B4980 /* RemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194E4B828785BFD00EB6307 /* RemoteLogger.swift */; }; + D242C29F2A14D6A7004B4980 /* RemoteLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194E4B828785BFD00EB6307 /* RemoteLogger.swift */; }; + D242C2A12A14D747004B4980 /* RemoteLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D242C2A02A14D747004B4980 /* RemoteLoggerTests.swift */; }; + D242C2A22A14D747004B4980 /* RemoteLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D242C2A02A14D747004B4980 /* RemoteLoggerTests.swift */; }; + D2432CF929EDB22C00D93657 /* Flushable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2432CF829EDB22C00D93657 /* Flushable.swift */; }; + D2432CFA29EDB22C00D93657 /* Flushable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2432CF829EDB22C00D93657 /* Flushable.swift */; }; + D243BBC0276C9D640019C857 /* PLCrashReporterIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBBF276C9D640019C857 /* PLCrashReporterIntegrationTests.swift */; }; + D243BBEC29A614CE000B9CEC /* LoggerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBEB29A614CE000B9CEC /* LoggerProtocol.swift */; }; + D243BBED29A614CE000B9CEC /* LoggerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBEB29A614CE000B9CEC /* LoggerProtocol.swift */; }; + D243BBF229A6209C000B9CEC /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBF129A6209C000B9CEC /* RequestBuilder.swift */; }; + D243BBF329A6209C000B9CEC /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBF129A6209C000B9CEC /* RequestBuilder.swift */; }; + D243BBF529A620CC000B9CEC /* MessageReceivers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBF429A620CC000B9CEC /* MessageReceivers.swift */; }; + D243BBF629A620CC000B9CEC /* MessageReceivers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBF429A620CC000B9CEC /* MessageReceivers.swift */; }; + D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */; }; + D24C9C3F29A79772002057CF /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C3E29A79772002057CF /* Logger.swift */; }; + D24C9C4029A79772002057CF /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C3E29A79772002057CF /* Logger.swift */; }; + D24C9C4229A7A50D002057CF /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; + D24C9C4329A7A50D002057CF /* DatadogLogs.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D24C9C4629A7A520002057CF /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D20731B429A5279D00ECBF94 /* DatadogLogs.framework */; }; + D24C9C4729A7A520002057CF /* DatadogLogs.framework in ⚙️ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = D20731B429A5279D00ECBF94 /* DatadogLogs.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D24C9C4A29A7B35C002057CF /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; + D24C9C4B29A7B365002057CF /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D20731B429A5279D00ECBF94 /* DatadogLogs.framework */; }; + D24C9C4D29A7BA3F002057CF /* LogsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C4C29A7B9CA002057CF /* LogsMocks.swift */; }; + D24C9C4E29A7BA41002057CF /* LogsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C4C29A7B9CA002057CF /* LogsMocks.swift */; }; + D24C9C5229A7BD12002057CF /* SamplerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C5129A7BD12002057CF /* SamplerMock.swift */; }; + D24C9C5329A7BD12002057CF /* SamplerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C5129A7BD12002057CF /* SamplerMock.swift */; }; + D24C9C5529A7C5F3002057CF /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C5429A7C5F3002057CF /* DateProvider.swift */; }; + D24C9C5629A7C5F3002057CF /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C5429A7C5F3002057CF /* DateProvider.swift */; }; + D24C9C6029A7CB0A002057CF /* DatadogLogsFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */; }; + D24C9C6129A7CB0C002057CF /* DatadogLogsFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */; }; + D24C9C6429A7CB7B002057CF /* CrashLogReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF416125EE5FF400CE35EC /* CrashLogReceiverTests.swift */; }; + D24C9C6529A7CB7D002057CF /* CrashLogReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF416125EE5FF400CE35EC /* CrashLogReceiverTests.swift */; }; + D24C9C6929A7CE06002057CF /* DDErrorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */; }; + D24C9C6A29A7CE06002057CF /* DDErrorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */; }; + D24C9C7129A7D57A002057CF /* DirectoriesMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C7029A7D57A002057CF /* DirectoriesMock.swift */; }; + D24C9C7229A7D57A002057CF /* DirectoriesMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C9C7029A7D57A002057CF /* DirectoriesMock.swift */; }; + D25085102976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D250850F2976E30000E931C3 /* DatadogRemoteFeatureMock.swift */; }; + D25085112976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D250850F2976E30000E931C3 /* DatadogRemoteFeatureMock.swift */; }; + D253EE962B988CA90010B589 /* ViewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE952B988CA90010B589 /* ViewCache.swift */; }; + D253EE972B988CA90010B589 /* ViewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE952B988CA90010B589 /* ViewCache.swift */; }; + D253EE9B2B98B37B0010B589 /* ViewCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE982B98B3690010B589 /* ViewCacheTests.swift */; }; + D253EE9C2B98B37C0010B589 /* ViewCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253EE982B98B3690010B589 /* ViewCacheTests.swift */; }; + D2552AF32BBC47D300A45725 /* WebRecordIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */; }; + D2552AF52BBC492400A45725 /* WebEventIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */; }; + D2552AF62BBC492600A45725 /* WebEventIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */; }; + D2553807288AA84F00727FAD /* UploadMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553806288AA84F00727FAD /* UploadMock.swift */; }; + D2553808288AA84F00727FAD /* UploadMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553806288AA84F00727FAD /* UploadMock.swift */; }; + D2553826288F0B1A00727FAD /* BatteryStatusPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553825288F0B1A00727FAD /* BatteryStatusPublisher.swift */; }; + D2553827288F0B1A00727FAD /* BatteryStatusPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553825288F0B1A00727FAD /* BatteryStatusPublisher.swift */; }; + D2553829288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */; }; + D255382A288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */; }; + D2579552298ABB04008A1BE5 /* FileWriterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579547298ABB04008A1BE5 /* FileWriterMock.swift */; }; + D2579553298ABB04008A1BE5 /* DatadogContextMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579548298ABB04008A1BE5 /* DatadogContextMock.swift */; }; + D2579554298ABB04008A1BE5 /* FeatureBaggageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579549298ABB04008A1BE5 /* FeatureBaggageMock.swift */; }; + D2579555298ABB04008A1BE5 /* PassthroughCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954A298ABB04008A1BE5 /* PassthroughCoreMock.swift */; }; + D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954B298ABB04008A1BE5 /* FoundationMocks.swift */; }; + D2579557298ABB04008A1BE5 /* AttributesMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954C298ABB04008A1BE5 /* AttributesMocks.swift */; }; + D2579558298ABB04008A1BE5 /* Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954E298ABB04008A1BE5 /* Encoding.swift */; }; + D2579559298ABB04008A1BE5 /* DDAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954F298ABB04008A1BE5 /* DDAssert.swift */; }; + D257955A298ABB04008A1BE5 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579550298ABB04008A1BE5 /* SwiftExtensions.swift */; }; + D257955B298ABB04008A1BE5 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579551298ABB04008A1BE5 /* XCTestCase.swift */; }; + D2579578298ABB83008A1BE5 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579551298ABB04008A1BE5 /* XCTestCase.swift */; }; + D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954B298ABB04008A1BE5 /* FoundationMocks.swift */; }; + D257957A298ABB83008A1BE5 /* DatadogContextMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579548298ABB04008A1BE5 /* DatadogContextMock.swift */; }; + D257957B298ABB83008A1BE5 /* Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954E298ABB04008A1BE5 /* Encoding.swift */; }; + D257957C298ABB83008A1BE5 /* DDAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954F298ABB04008A1BE5 /* DDAssert.swift */; }; + D257957D298ABB83008A1BE5 /* FileWriterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579547298ABB04008A1BE5 /* FileWriterMock.swift */; }; + D257957E298ABB83008A1BE5 /* FeatureBaggageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579549298ABB04008A1BE5 /* FeatureBaggageMock.swift */; }; + D257957F298ABB83008A1BE5 /* AttributesMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954C298ABB04008A1BE5 /* AttributesMocks.swift */; }; + D2579580298ABB83008A1BE5 /* PassthroughCoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D257954A298ABB04008A1BE5 /* PassthroughCoreMock.swift */; }; + D2579581298ABB83008A1BE5 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2579550298ABB04008A1BE5 /* SwiftExtensions.swift */; }; + D2579592298ABCED008A1BE5 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2579591298ABCED008A1BE5 /* XCTest.framework */; }; + D2579595298AC912008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D2579596298AC927008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; + D2579599298AD95F008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D257959A298AD967008A1BE5 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; + D25C834C2B8657CF008E73B1 /* SegmentJSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25C834B2B8657CF008E73B1 /* SegmentJSONTests.swift */; }; + D25CFA9829C4F41900E3A43D /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; + D25CFA9929C4F41900E3A43D /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; + D25CFA9C29C4FC6900E3A43D /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; }; + D25CFA9D29C4FC6E00E3A43D /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; }; + D25CFA9F29C860E100E3A43D /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25CFA9E29C85FA400E3A43D /* TracingFeatureMocks.swift */; }; + D25CFAA029C860E300E3A43D /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25CFA9E29C85FA400E3A43D /* TracingFeatureMocks.swift */; }; + D25CFAA229C8644E00E3A43D /* Casting+Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25CFAA129C8644E00E3A43D /* Casting+Tracing.swift */; }; + D25CFAA329C8644E00E3A43D /* Casting+Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25CFAA129C8644E00E3A43D /* Casting+Tracing.swift */; }; + D25EE93C29C4C3C300CE3839 /* DatadogTrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; platformFilter = ios; }; + D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; + D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */; }; + D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */; }; + D263BCB429DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */; }; + D263BCB529DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */; }; + D263BCB629DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */; }; + D263BCB729DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */; }; + D26416B62A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26416B52A30E84F00BCD9F7 /* CoreRegistryTest.swift */; }; + D26416B72A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26416B52A30E84F00BCD9F7 /* CoreRegistryTest.swift */; }; + D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; + D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; + D26C49BF288982DA00802B2D /* FeatureUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49BE288982DA00802B2D /* FeatureUpload.swift */; }; + D26C49C0288982DA00802B2D /* FeatureUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49BE288982DA00802B2D /* FeatureUpload.swift */; }; + D26F741129ACBDA100D25622 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D26F741229ACBDAD00D25622 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D270CDDD2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */; }; + 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 */; }; + D27D81C12A5D415200281CC2 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + D27D81C22A5D415200281CC2 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; + D27D81C32A5D415200281CC2 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D27D81C42A5D415200281CC2 /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; + D27D81C52A5D415200281CC2 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + 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 */; }; + D28F836929C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */; }; + D28F836B29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */; }; + D28F836C29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */; }; + D28FCC352B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D28FCC342B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy */; }; + D28FCC362B5FCBD100CCC077 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D28FCC342B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy */; }; + D29294E0291D5ED100F8EFF9 /* ApplicationVersionPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29294DF291D5ECD00F8EFF9 /* ApplicationVersionPublisher.swift */; }; + D29294E1291D5ED500F8EFF9 /* ApplicationVersionPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29294DF291D5ECD00F8EFF9 /* ApplicationVersionPublisher.swift */; }; + D29294E3291D652C00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29294E2291D652900F8EFF9 /* ApplicationVersionPublisherTests.swift */; }; + D29294E4291D652D00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29294E2291D652900F8EFF9 /* ApplicationVersionPublisherTests.swift */; }; + D293302C2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D293302B2A137DAD0029C9EA /* CrashReportingFeature.swift */; }; + D293302D2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D293302B2A137DAD0029C9EA /* CrashReportingFeature.swift */; }; + D295A16529F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */; }; + D295A16629F299C9007C0E9A /* URLSessionInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */; }; + D29732492A5C108700827599 /* DDScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29732462A5C108700827599 /* DDScriptMessageHandler.swift */; }; + D297324B2A5C108700827599 /* MessageEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29732472A5C108700827599 /* MessageEmitter.swift */; }; + D29732512A5C109A00827599 /* MessageEmitterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D297324F2A5C109A00827599 /* MessageEmitterTests.swift */; }; + D29732532A5C109A00827599 /* WebViewTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29732502A5C109A00827599 /* WebViewTrackingTests.swift */; }; + D29A9F3C29DD84AB005C54A4 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; }; + D29A9F4B29DD8525005C54A4 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D29A9F5029DD85BA005C54A4 /* RUMContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63824BF19B4008053F2 /* RUMContext.swift */; }; + D29A9F5129DD85BB005C54A4 /* LongTaskObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */; }; + D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFD824C7269500589570 /* RUMUUIDGenerator.swift */; }; + D29A9F5329DD85BB005C54A4 /* RUMOffViewEventsHandlingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */; }; + D29A9F5429DD85BB005C54A4 /* RUMScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63624BF191F008053F2 /* RUMScope.swift */; }; + D29A9F5529DD85BB005C54A4 /* PerformanceMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = E179FB4D28F80A6400CC2698 /* PerformanceMetric.swift */; }; + D29A9F5729DD85BB005C54A4 /* URLSessionRUMResourcesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB11E29D30AF000737A9A /* URLSessionRUMResourcesHandler.swift */; }; + D29A9F5829DD85BB005C54A4 /* RUMConnectivityInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614B0A4E24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift */; }; + D29A9F5929DD85BB005C54A4 /* RUMCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63A24BF1A4B008053F2 /* RUMCommand.swift */; }; + D29A9F5A29DD85BB005C54A4 /* RUMScopeDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6122514727FDFF82004F5AE4 /* RUMScopeDependencies.swift */; }; + D29A9F5B29DD85BB005C54A4 /* VitalMemoryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */; }; + D29A9F5C29DD85BB005C54A4 /* RUMSessionScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C20624C098FC00C0321C /* RUMSessionScope.swift */; }; + D29A9F5D29DD85BB005C54A4 /* RUMCommandSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */; }; + D29A9F5E29DD85BB005C54A4 /* UIKitRUMViewsPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA62512144600C816E5 /* UIKitRUMViewsPredicate.swift */; }; + D29A9F6029DD85BB005C54A4 /* ViewIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF9A4425AC5DEA001058CC /* ViewIdentifier.swift */; }; + D29A9F6129DD85BB005C54A4 /* CrashReportReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236BE2729520FED00676E67 /* CrashReportReceiver.swift */; }; + D29A9F6229DD85BB005C54A4 /* WebViewEventReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CBC26A294383F200134409 /* WebViewEventReceiver.swift */; }; + D29A9F6329DD85BB005C54A4 /* RUMMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333524B84B43003D6C4E /* RUMMonitor.swift */; }; + D29A9F6429DD85BB005C54A4 /* VitalInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */; }; + D29A9F6529DD85BB005C54A4 /* RUMUserActionScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */; }; + 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 /* 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 */; }; + D29A9F6E29DD85BB005C54A4 /* RUMUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFD624C7265300589570 /* RUMUUID.swift */; }; + D29A9F6F29DD85BB005C54A4 /* RUMInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */; }; + D29A9F7029DD85BB005C54A4 /* RUMContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6156CB8D24DDA1B5008CB2B2 /* RUMContextProvider.swift */; }; + D29A9F7129DD85BB005C54A4 /* RUMDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FD9FCB28533EDF00214BD9 /* RUMDeviceInfo.swift */; }; + D29A9F7329DD85BB005C54A4 /* RUMApplicationScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3E63D24BF1B91008053F2 /* RUMApplicationScope.swift */; }; + D29A9F7429DD85BB005C54A4 /* RUMFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2E729CC6B680063802D /* RUMFeature.swift */; }; + D29A9F7529DD85BB005C54A4 /* RUMViewScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C21124C5951400C0321C /* RUMViewScope.swift */; }; + D29A9F7629DD85BB005C54A4 /* RUMViewsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */; }; + D29A9F7729DD85BB005C54A4 /* RUMDebugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B22E5924F3E6B700DC26D2 /* RUMDebugging.swift */; }; + D29A9F7829DD85BB005C54A4 /* RUMDataModelsMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715F824DC13A100FC0F69 /* RUMDataModelsMapping.swift */; }; + D29A9F7929DD85BB005C54A4 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2ED29CC73240063802D /* RequestBuilder.swift */; }; + D29A9F7A29DD85BB005C54A4 /* VitalCPUReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */; }; + D29A9F7B29DD85BB005C54A4 /* RUMDataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E26E6B824C87693000B3270 /* RUMDataModels.swift */; }; + D29A9F7C29DD85BB005C54A4 /* RUMEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF281D24B8968D000B3D9B /* RUMEventBuilder.swift */; }; + D29A9F7D29DD85BB005C54A4 /* RUMEventsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E81EF25A740140084B751 /* RUMEventsMapper.swift */; }; + D29A9F7E29DD85BB005C54A4 /* UIViewControllerSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA42511190E00C816E5 /* UIViewControllerSwizzler.swift */; }; + D29A9F7F29DD85BB005C54A4 /* RUMEventSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122ED325B1B84D00F9C7F5 /* RUMEventSanitizer.swift */; }; + D29A9F8029DD85BB005C54A4 /* UIViewControllerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F3CDA2251118FB00C816E5 /* UIViewControllerHandler.swift */; }; + D29A9F8129DD85BB005C54A4 /* RUMConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2EA29CC6D6F0063802D /* RUMConfiguration.swift */; }; + D29A9F8229DD85BB005C54A4 /* UIKitRUMUserActionsPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */; }; + D29A9F8329DD85BB005C54A4 /* RUMBaggageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25FF2F329CC88060063802D /* RUMBaggageKeys.swift */; }; + D29A9F8429DD85BB005C54A4 /* RUMResourceScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB024C839460082C633 /* RUMResourceScope.swift */; }; + D29A9F8529DD85BB005C54A4 /* SwiftUIViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24985A12728048B00B4F72D /* SwiftUIViewHandler.swift */; }; + D29A9F8629DD85BB005C54A4 /* SessionReplayDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950ED291C058F00470E0C /* SessionReplayDependency.swift */; }; + D29A9F8729DD85BB005C54A4 /* SwiftUIActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */; }; + D29A9F8829DD85BB005C54A4 /* ErrorMessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */; }; + D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */; }; + D29A9F8C29DD861C005C54A4 /* ValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611529A425E3DD51004F740E /* ValuePublisher.swift */; }; + D29A9F8D29DD8665005C54A4 /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */; }; + D29A9F8E29DD8665005C54A4 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */; }; + D29A9F9029DD876F005C54A4 /* CITestIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11625D727B681D200E428C6 /* CITestIntegration.swift */; }; + D29A9F9129DD8771005C54A4 /* CITestIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11625D727B681D200E428C6 /* CITestIntegration.swift */; }; + D29A9F9529DDB1DB005C54A4 /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */; }; + D29A9F9629DDB1DB005C54A4 /* UIKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */; }; + D29A9F9A29DDB483005C54A4 /* URLSessionRUMResourcesHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB12129D34A5F00737A9A /* URLSessionRUMResourcesHandlerTests.swift */; }; + D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611529AD25E3E429004F740E /* ValuePublisherTests.swift */; }; + D29A9F9F29DDB483005C54A4 /* RUMApplicationScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */; }; + D29A9FA229DDB483005C54A4 /* RUMEventSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */; }; + D29A9FA329DDB483005C54A4 /* RUMDeviceInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */; }; + D29A9FA429DDB483005C54A4 /* WebViewEventReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53889B2773C4B300A7DC42 /* WebViewEventReceiverTests.swift */; }; + D29A9FA629DDB483005C54A4 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */; }; + 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 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; }; + D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950EA291C029700470E0C /* SessionReplayDependencyTests.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 */; }; + D29A9FB929DDB483005C54A4 /* RUMEventsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E81F625A743600084B751 /* RUMEventsMapperTests.swift */; }; + D29A9FBB29DDB483005C54A4 /* ErrorMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */; }; + D29A9FBC29DDB483005C54A4 /* RUMResourceScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */; }; + D29A9FBD29DDB483005C54A4 /* RUMSessionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */; }; + D29A9FBE29DDB483005C54A4 /* RUMEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */; }; + D29A9FC029DDB540005C54A4 /* RUMFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5333024B75DFC003D6C4E /* RUMFeatureMocks.swift */; }; + D29A9FC129DDB58C005C54A4 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; platformFilter = ios; }; + D29A9FC429DDB710005C54A4 /* RUMInternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */; }; + D29A9FC529DDB719005C54A4 /* RUMInternalProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */; }; + D29A9FC629DDBA8A005C54A4 /* RUMDataModelMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E820425A879AF0084B751 /* RUMDataModelMocks.swift */; }; + D29A9FCC29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FCB29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift */; }; + D29A9FCE29DDC4BA005C54A4 /* RUMFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */; }; + D29A9FCF29DDC4BC005C54A4 /* RUMFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */; }; + D29A9FD029DDC58E005C54A4 /* RUMFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */; }; + D29A9FD129DDC590005C54A4 /* RUMFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */; }; + D29A9FD529DDC624005C54A4 /* RUMDataModelsMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715FB24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift */; }; + D29A9FD829DDC686005C54A4 /* UIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */; }; + D29A9FD929DDC687005C54A4 /* UIKitRUMViewsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */; }; + 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 */; }; + 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 */; }; + D2A1EE36287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */; }; + D2A1EE38287EEB7400D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */; }; + D2A1EE39287EEB7600D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */; }; + D2A1EE3B287EECC000D28DFB /* CarrierInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */; }; + D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */; }; + D2A1EE3E2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */; }; + 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 */; }; + 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 */; }; + D2A783DA29A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; + D2A783E729A53468003B03BB /* LogEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */; }; + D2A783E829A53468003B03BB /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194D51B287ECDC00091547D /* ConsoleLoggerTests.swift */; }; + D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */; }; + D2A783EB29A53468003B03BB /* LogSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3C2423990D00786299 /* LogSanitizerTests.swift */; }; + D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; + D2A783F329A534F9003B03BB /* ConsoleLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6194D51B287ECDC00091547D /* ConsoleLoggerTests.swift */; }; + D2A783F429A534F9003B03BB /* LogSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3C2423990D00786299 /* LogSanitizerTests.swift */; }; + D2A783F529A534F9003B03BB /* LogEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */; }; + D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; + D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */; }; + D2A783FB29A534F9003B03BB /* DatadogLogs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; }; + D2A7840329A536AD003B03BB /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */; }; + D2A7840429A536AD003B03BB /* PrintFunctionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */; }; + D2A7840529A5370A003B03BB /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; platformFilter = ios; }; + D2A7840629A53710003B03BB /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; + D2A7840D29A53A4B003B03BB /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; + D2A7840E29A53A4B003B03BB /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; + D2A7840F29A53B2F003B03BB /* Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAB2423979B00786299 /* Directory.swift */; }; + D2A7841029A53B2F003B03BB /* Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAB2423979B00786299 /* Directory.swift */; }; + D2A7841129A53B2F003B03BB /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAC2423979B00786299 /* File.swift */; }; + D2A7841229A53B2F003B03BB /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAC2423979B00786299 /* File.swift */; }; + D2A7A8FF2BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */; }; + 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 */; }; + D2B249982A45E10500DD4F9F /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B249962A45E10500DD4F9F /* LoggerTests.swift */; }; + D2B3F0442823EE8400C2B5EE /* TLVBlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F0432823EE8300C2B5EE /* TLVBlockTests.swift */; }; + D2B3F0452823EE8400C2B5EE /* TLVBlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F0432823EE8300C2B5EE /* TLVBlockTests.swift */; }; + D2B3F04D282A85FD00C2B5EE /* DatadogCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */; }; + D2B3F04E282A85FD00C2B5EE /* DatadogCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */; }; + D2B3F052282E827700C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */; }; + D2B3F053282E827B00C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */; }; + D2BCB2A12B7B8107005C2AAB /* WKWebViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB2A02B7B8107005C2AAB /* WKWebViewRecorder.swift */; }; + D2BCB2A32B7B9683005C2AAB /* WKWebViewRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BCB2A22B7B9683005C2AAB /* WKWebViewRecorderTests.swift */; }; + D2BEEDAC2B3356710065F3AC /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDAB2B3356710065F3AC /* URLSessionTaskSwizzler.swift */; }; + D2BEEDAD2B3356710065F3AC /* URLSessionTaskSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDAB2B3356710065F3AC /* URLSessionTaskSwizzler.swift */; }; + D2BEEDAF2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDAE2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift */; }; + D2BEEDB02B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDAE2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift */; }; + D2BEEDB22B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB12B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift */; }; + D2BEEDB32B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB12B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift */; }; + D2BEEDB52B3360820065F3AC /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB42B33607D0065F3AC /* URLSessionSwizzler.swift */; }; + D2BEEDB62B3360830065F3AC /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB42B33607D0065F3AC /* URLSessionSwizzler.swift */; }; + D2BEEDB82B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */; }; + D2BEEDB92B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */; }; + D2BEEDBA2B33638F0065F3AC /* NetworkInstrumentationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2181A8A2B0500BB00A518C0 /* NetworkInstrumentationSwizzler.swift */; }; + D2BEEDBB2B3363900065F3AC /* NetworkInstrumentationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2181A8A2B0500BB00A518C0 /* NetworkInstrumentationSwizzler.swift */; }; + D2C1A4FA29C4C4CB00946C31 /* SpanSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */; }; + D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0A29AF56270054E00B /* MessageReceivers.swift */; }; + D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0729AF55E90054E00B /* RequestBuilder.swift */; }; + D2C1A4FE29C4C4CB00946C31 /* SpanEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618D9DE6263AD78900A3FAD2 /* SpanEventMapper.swift */; }; + D2C1A4FF29C4C4CB00946C31 /* Warnings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87D24509A0C00DA608C /* Warnings.swift */; }; + D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D202E924C065CF00D1AF3A /* ActiveSpansPool.swift */; }; + D2C1A50129C4C4CB00946C31 /* DDSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */; }; + D2C1A50229C4C4CB00946C31 /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87C24509A0C00DA608C /* Casting.swift */; }; + D2C1A50329C4C4CB00946C31 /* DDFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDD629B8F08E00B15732 /* DDFormat.swift */; }; + D2C1A50429C4C4CB00946C31 /* (null) in Sources */ = {isa = PBXBuildFile; }; + D2C1A50529C4C4CB00946C31 /* DatadogTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546BF029AF4F550054E00B /* DatadogTracer.swift */; }; + D2C1A50629C4C4CB00946C31 /* TracingWithLoggingIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216275247D1CD700AC5D67 /* TracingWithLoggingIntegration.swift */; }; + D2C1A50729C4C4CB00946C31 /* DDSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87824509A0C00DA608C /* DDSpan.swift */; }; + D2C1A50829C4C4CB00946C31 /* TracingURLSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25BADA029C1EF3000112069 /* TracingURLSessionHandler.swift */; }; + D2C1A50929C4C4CB00946C31 /* SpanEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */; }; + D2C1A50A29C4C4CB00946C31 /* TraceFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0329AF55AA0054E00B /* TraceFeature.swift */; }; + D2C1A50B29C4C4CB00946C31 /* SpanEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */; }; + D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87924509A0C00DA608C /* DDNoOps.swift */; }; + D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614872762485067300E3EBDB /* SpanTagsReducer.swift */; }; + D2C1A50E29C4C4EF00946C31 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; }; + D2C1A51329C4C53F00946C31 /* OTReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EA24A24DD3005EA2DE /* OTReference.swift */; }; + D2C1A51429C4C53F00946C31 /* OTSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */; }; + D2C1A51529C4C53F00946C31 /* OTTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E924A24DD3005EA2DE /* OTTracer.swift */; }; + D2C1A51629C4C53F00946C31 /* OTConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EB24A24DD3005EA2DE /* OTConstants.swift */; }; + D2C1A51729C4C53F00946C31 /* OTFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E724A24DD3005EA2DE /* OTFormat.swift */; }; + D2C1A51829C4C53F00946C31 /* OTSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E624A24DD3005EA2DE /* OTSpan.swift */; }; + D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A620249A45E400075390 /* DDSpanContextTests.swift */; }; + D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E8D59728C7AB90007E5DE1 /* ContextMessageReceiverTests.swift */; }; + D2C1A51D29C4C75700946C31 /* SpanEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */; }; + D2C1A51E29C4C75700946C31 /* Casting+Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89C24509C1100DA608C /* Casting+Tracing.swift */; }; + D2C1A51F29C4C75700946C31 /* ActiveSpansPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D203FB24C1884500D1AF3A /* ActiveSpansPoolTests.swift */; }; + D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89824509C1100DA608C /* DDSpanTests.swift */; }; + D2C1A52229C4C75700946C31 /* DDNoopTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */; }; + D2C1A52329C4C75700946C31 /* WarningsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89A24509C1100DA608C /* WarningsTests.swift */; }; + D2C1A52429C4C75700946C31 /* TracingURLSessionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A38DDA29C37E1B007C6900 /* TracingURLSessionHandlerTests.swift */; }; + D2C1A52529C4C75700946C31 /* SpanSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122EE725B1C92500F9C7F5 /* SpanSanitizerTests.swift */; }; + D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */; }; + D2C1A52829C4C8CB00946C31 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; }; + D2C1A53829C4F2DF00946C31 /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87C24509A0C00DA608C /* Casting.swift */; }; + D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87924509A0C00DA608C /* DDNoOps.swift */; }; + D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0729AF55E90054E00B /* RequestBuilder.swift */; }; + D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614872762485067300E3EBDB /* SpanTagsReducer.swift */; }; + D2C1A53D29C4F2DF00946C31 /* OTSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E624A24DD3005EA2DE /* OTSpan.swift */; }; + D2C1A53E29C4F2DF00946C31 /* OTSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */; }; + D2C1A53F29C4F2DF00946C31 /* OTReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EA24A24DD3005EA2DE /* OTReference.swift */; }; + D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0A29AF56270054E00B /* MessageReceivers.swift */; }; + D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D202E924C065CF00D1AF3A /* ActiveSpansPool.swift */; }; + D2C1A54329C4F2DF00946C31 /* SpanEventEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */; }; + D2C1A54429C4F2DF00946C31 /* SpanEventMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618D9DE6263AD78900A3FAD2 /* SpanEventMapper.swift */; }; + D2C1A54529C4F2DF00946C31 /* DDFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDD629B8F08E00B15732 /* DDFormat.swift */; }; + D2C1A54629C4F2DF00946C31 /* OTConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EB24A24DD3005EA2DE /* OTConstants.swift */; }; + D2C1A54729C4F2DF00946C31 /* DDSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */; }; + D2C1A54829C4F2DF00946C31 /* OTTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E924A24DD3005EA2DE /* OTTracer.swift */; }; + D2C1A54929C4F2DF00946C31 /* (null) in Sources */ = {isa = PBXBuildFile; }; + D2C1A54A29C4F2DF00946C31 /* DDSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87824509A0C00DA608C /* DDSpan.swift */; }; + D2C1A54B29C4F2DF00946C31 /* TracingWithLoggingIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216275247D1CD700AC5D67 /* TracingWithLoggingIntegration.swift */; }; + D2C1A54C29C4F2DF00946C31 /* SpanEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */; }; + D2C1A54D29C4F2DF00946C31 /* Warnings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87D24509A0C00DA608C /* Warnings.swift */; }; + D2C1A54E29C4F2DF00946C31 /* OTFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E724A24DD3005EA2DE /* OTFormat.swift */; }; + D2C1A54F29C4F2DF00946C31 /* SpanSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */; }; + D2C1A55029C4F2DF00946C31 /* TraceFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546C0329AF55AA0054E00B /* TraceFeature.swift */; }; + D2C1A55129C4F2DF00946C31 /* TracingURLSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25BADA029C1EF3000112069 /* TracingURLSessionHandler.swift */; }; + D2C1A55229C4F2DF00946C31 /* DatadogTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2546BF029AF4F550054E00B /* DatadogTracer.swift */; }; + D2C1A55F29C4F2E800946C31 /* Casting+Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89C24509C1100DA608C /* Casting+Tracing.swift */; }; + D2C1A56029C4F2E800946C31 /* TracingURLSessionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A38DDA29C37E1B007C6900 /* TracingURLSessionHandlerTests.swift */; }; + D2C1A56129C4F2E800946C31 /* WarningsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89A24509C1100DA608C /* WarningsTests.swift */; }; + D2C1A56229C4F2E800946C31 /* SpanEventBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */; }; + D2C1A56329C4F2E800946C31 /* DDNoopTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */; }; + D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E8D59728C7AB90007E5DE1 /* ContextMessageReceiverTests.swift */; }; + D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89824509C1100DA608C /* DDSpanTests.swift */; }; + D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A620249A45E400075390 /* DDSpanContextTests.swift */; }; + D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */; }; + D2C1A56929C4F2E800946C31 /* ActiveSpansPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D203FB24C1884500D1AF3A /* ActiveSpansPoolTests.swift */; }; + D2C1A56A29C4F2E800946C31 /* SpanSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61122EE725B1C92500F9C7F5 /* SpanSanitizerTests.swift */; }; + D2C1A57429C4F30000946C31 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D2C5D5282B83FD5300B63F36 /* WebViewMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */; }; + D2C5D5292B83FD5400B63F36 /* WebViewMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */; }; + D2C5D52B2B84F6AB00B63F36 /* WebViewRecordReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */; }; + D2C5D52D2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */; }; + D2C5D5302B84F71200B63F36 /* WebRecordIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */; }; + D2C7E3AB28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */; }; + D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; + D2C9A26A2C0F3F5A007526F5 /* SessionReplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EA0F452C0E1AE200CB20F8 /* SessionReplayConfiguration.swift */; }; + D2C9A2872C0F467C007526F5 /* SessionReplayConfigurationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C9A2852C0F4660007526F5 /* SessionReplayConfigurationMocks.swift */; }; + D2C9A2882C0F467C007526F5 /* SessionReplayConfigurationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C9A2852C0F4660007526F5 /* SessionReplayConfigurationMocks.swift */; }; + D2CB6E0C27C50EAE00A62B57 /* DatadogCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133B85242393DE00786299 /* DatadogCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D2CB6E0D27C50EAE00A62B57 /* ObjcAppLaunchHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D2CB6E0E27C50EAE00A62B57 /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D2CB6E2927C50EAE00A62B57 /* KronosInternetAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */; }; + D2CB6E2C27C50EAE00A62B57 /* KronosNTPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */; }; + D2CB6E3127C50EAE00A62B57 /* FileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA72423979B00786299 /* FileWriter.swift */; }; + 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 */; }; + 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 */; }; + D2CB6E7627C50EAE00A62B57 /* KronosClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CC277B23F0008BE766 /* KronosClock.swift */; }; + D2CB6E7727C50EAE00A62B57 /* DataReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E793A2577B6EE00DFCC17 /* DataReader.swift */; }; + D2CB6E8127C50EAE00A62B57 /* DataUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB02423979B00786299 /* DataUploader.swift */; }; + D2CB6E8827C50EAE00A62B57 /* FileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAD2423979B00786299 /* FileReader.swift */; }; + D2CB6E8D27C50EAE00A62B57 /* KronosNTPProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */; }; + D2CB6E9127C50EAE00A62B57 /* KronosTimeFreeze.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */; }; + D2CB6E9727C50EAE00A62B57 /* DataUploadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */; }; + D2CB6E9927C50EAE00A62B57 /* DataUploadWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB12423979B00786299 /* DataUploadWorker.swift */; }; + D2CB6E9A27C50EAE00A62B57 /* KronosTimeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */; }; + D2CB6E9B27C50EAE00A62B57 /* FilesOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BA92423979B00786299 /* FilesOrchestrator.swift */; }; + D2CB6EA727C50EAE00A62B57 /* Versioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5AEA624B4D45A007F194B /* Versioning.swift */; }; + D2CB6EA827C50EAE00A62B57 /* URLSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB22423979B00786299 /* URLSessionClient.swift */; }; + D2CB6EB327C50EAE00A62B57 /* KronosNTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */; }; + D2CB6EBA27C50EAE00A62B57 /* DataUploadConditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BAF2423979B00786299 /* DataUploadConditions.swift */; }; + D2CB6EBF27C50EAE00A62B57 /* KronosData+Bytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */; }; + D2CB6EC427C50EAE00A62B57 /* DataUploadDelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133BB32423979B00786299 /* DataUploadDelay.swift */; }; + D2CB6EC727C50EAE00A62B57 /* PerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */; }; + D2CB6EDE27C520D400A62B57 /* RUMEventMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FF282724B8A31E000B3D9B /* RUMEventMatcher.swift */; }; + D2CB6EE027C520D400A62B57 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; }; + D2CB6EE427C520D400A62B57 /* FeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EF78C0257F842000EDCCB3 /* FeatureTests.swift */; }; + D2CB6EE527C520D400A62B57 /* DataUploadConditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C302423990D00786299 /* DataUploadConditionsTests.swift */; }; + D2CB6EE627C520D400A62B57 /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C365E248E85B400520CDE /* DateFormattingTests.swift */; }; + D2CB6EE727C520D400A62B57 /* FileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2C2423990D00786299 /* FileTests.swift */; }; + D2CB6EEA27C520D400A62B57 /* LogMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C432423990D00786299 /* LogMatcher.swift */; }; + D2CB6EEC27C520D400A62B57 /* CustomObjcViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */; }; + D2CB6EEE27C520D400A62B57 /* DDErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */; }; + D2CB6EF227C520D400A62B57 /* KronosTimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */; }; + 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 */; }; + 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 */; }; + D2CB6F0E27C520D400A62B57 /* DDRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */; }; + D2CB6F1027C520D400A62B57 /* DDNSURLSessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */; }; + D2CB6F1327C520D400A62B57 /* DDConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C162423990D00786299 /* DDConfigurationTests.swift */; }; + D2CB6F1727C520D400A62B57 /* ObjcExceptionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */; }; + D2CB6F1827C520D400A62B57 /* DatadogTestsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6184751426EFCF1300C7C9C5 /* DatadogTestsObserver.swift */; }; + D2CB6F1927C520D400A62B57 /* RequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C332423990D00786299 /* RequestBuilderTests.swift */; }; + D2CB6F1A27C520D400A62B57 /* FileReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C282423990D00786299 /* FileReaderTests.swift */; }; + D2CB6F1D27C520D400A62B57 /* DataUploaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C322423990D00786299 /* DataUploaderTests.swift */; }; + D2CB6F2027C520D400A62B57 /* DatadogConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BBD19624ED50040023E65F /* DatadogConfigurationTests.swift */; }; + D2CB6F2127C520D400A62B57 /* URLSessionClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C342423990D00786299 /* URLSessionClientTests.swift */; }; + D2CB6F2227C520D400A62B57 /* DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C412423990D00786299 /* DatadogTests.swift */; }; + D2CB6F2627C520D400A62B57 /* DataUploadDelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C312423990D00786299 /* DataUploadDelayTests.swift */; }; + D2CB6F2827C520D400A62B57 /* DataUploadWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */; }; + D2CB6F2B27C520D400A62B57 /* CrashContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FC5F3425CC1898006BB4DE /* CrashContextProviderTests.swift */; }; + D2CB6F2C27C520D400A62B57 /* JSONEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */; }; + D2CB6F3027C520D400A62B57 /* DatadogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C472423990D00786299 /* DatadogExtensions.swift */; }; + D2CB6F3227C520D400A62B57 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; }; + D2CB6F3327C520D400A62B57 /* FilesOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2A2423990D00786299 /* FilesOrchestratorTests.swift */; }; + D2CB6F3B27C520D400A62B57 /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */; }; + D2CB6F4327C520D400A62B57 /* DDLogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C172423990D00786299 /* DDLogsTests.swift */; }; + D2CB6F4527C520D400A62B57 /* TracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89524509BF600DA608C /* TracerTests.swift */; }; + D2CB6F4627C520D400A62B57 /* CoreMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A6192498A51700075390 /* CoreMocks.swift */; }; + D2CB6F4827C520D400A62B57 /* CrashReportingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */; }; + D2CB6F4D27C520D400A62B57 /* DataUploadStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */; }; + D2CB6F4F27C520D400A62B57 /* RetryingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */; }; + D2CB6F5027C520D400A62B57 /* DDDatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C142423990D00786299 /* DDDatadogTests.swift */; }; + D2CB6F5327C520D400A62B57 /* DirectoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C2D2423990D00786299 /* DirectoryTests.swift */; }; + D2CB6F5F27C520D400A62B57 /* DDNSURLSessionDelegate+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42A26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m */; }; + D2CB6F6427C520D400A62B57 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C382423990D00786299 /* LoggerTests.swift */; }; + D2CB6F6627C520D400A62B57 /* RUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */; }; + D2CB6F6827C520D400A62B57 /* SwiftUIExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */; }; + D2CB6F6A27C520D400A62B57 /* DDRUMMonitor+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42026DF85C7000B0A5F /* DDRUMMonitor+apiTests.m */; }; + D2CB6F7027C520D400A62B57 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1C2423990D00786299 /* UIKitMocks.swift */; }; + D2CB6F7327C520D400A62B57 /* CoreTelephonyMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */; }; + D2CB6F7527C520D400A62B57 /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; + D2CB6F7C27C520D400A62B57 /* CrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2724825C943C500D54BF8 /* CrashReporterTests.swift */; }; + D2CB6F7D27C520D400A62B57 /* CrashContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6172472625D673D7007085B3 /* CrashContextTests.swift */; }; + D2CB6F7E27C520D400A62B57 /* OTSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BAD46926415FCE001886CA /* OTSpanTests.swift */; }; + D2CB6F7F27C520D400A62B57 /* DDDatadog+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42626DFB145000B0A5F /* DDDatadog+apiTests.m */; }; + D2CB6F8027C520D400A62B57 /* TracingWithLoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */; }; + D2CB6F8327C520D400A62B57 /* DDConfiguration+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5E42826DFB60A000B0A5F /* DDConfiguration+apiTests.m */; }; + 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, ); }; }; + 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 */; }; + D2CB6F9C27C5217A00A62B57 /* OTTracer+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */; }; + D2CB6F9E27C5217A00A62B57 /* Datadog+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C092423983800786299 /* Datadog+objc.swift */; }; + D2CB6F9F27C5217A00A62B57 /* Logs+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0C2423983800786299 /* Logs+objc.swift */; }; + D2CB6FA027C5217A00A62B57 /* Trace+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Trace+objc.swift */; }; + D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */; }; + D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */; }; + D2CB6FA327C5217A00A62B57 /* OTSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8A24A3568900233986 /* OTSpan+objc.swift */; }; + D2CB6FA427C5217A00A62B57 /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; + D2CB6FA527C5217A00A62B57 /* RUM+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E55407B25812D1C00F6E3AD /* RUM+objc.swift */; }; + D2CB6FA627C5217A00A62B57 /* OTSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */; }; + D2CB6FA827C5217A00A62B57 /* DatadogConfiguration+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */; }; + D2CB6FB327C5234300A62B57 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D2CB6FB827C523DA00A62B57 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; }; + D2CB6FB927C523DA00A62B57 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */; }; + D2CB6FBE27C5348200A62B57 /* DatadogCrashReporting.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B7885625C180CB002675B5 /* DatadogCrashReporting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D2CB6FC027C5348200A62B57 /* DDCrashReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */; }; + D2CB6FC127C5348200A62B57 /* DDCrashReportExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612556BA268DD9BF002BCE74 /* DDCrashReportExporter.swift */; }; + D2CB6FC227C5348200A62B57 /* CrashReportMinifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA1226971953001D9D43 /* CrashReportMinifier.swift */; }; + D2CB6FC327C5348200A62B57 /* PLCrashReporterIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2728A25C9561A00D54BF8 /* PLCrashReporterIntegration.swift */; }; + D2CB6FC427C5348200A62B57 /* CrashReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612556AF268C8D31002BCE74 /* CrashReport.swift */; }; + D2CB6FC527C5348200A62B57 /* SwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */; }; + D2CB6FC627C5348200A62B57 /* PLCrashReporterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6170DC1B25C18729003AED5C /* PLCrashReporterPlugin.swift */; }; + D2CB6FC727C5348200A62B57 /* ThirdPartyCrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2727325C9509D00D54BF8 /* ThirdPartyCrashReporter.swift */; }; + D2CB6FCB27C5348200A62B57 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + D2CB6FD927C5352300A62B57 /* DDCrashReportBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA1626974CA9001D9D43 /* DDCrashReportBuilderTests.swift */; }; + D2CB6FDB27C5352300A62B57 /* SwiftExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */; }; + D2CB6FDC27C5352300A62B57 /* PLCrashReporterIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243BBBF276C9D640019C857 /* PLCrashReporterIntegrationTests.swift */; }; + D2CB6FDD27C5352300A62B57 /* CrashReportMinifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA14269722B4001D9D43 /* CrashReportMinifierTests.swift */; }; + D2CB6FDE27C5352300A62B57 /* DDCrashReportExporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */; }; + D2CB6FE027C5352300A62B57 /* CrashReportingPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B7886125C180CB002675B5 /* CrashReportingPluginTests.swift */; }; + D2CB6FE127C5352300A62B57 /* CrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615CC4122695957C0005F08C /* CrashReportTests.swift */; }; + D2CB6FE227C5352300A62B57 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2729A25C95EB200D54BF8 /* Mocks.swift */; }; + D2CB6FE527C5352300A62B57 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + D2CB6FF327C5369600A62B57 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */; }; + D2CE604229911EDE00DB6656 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D2D30E5B2A40BF540020C553 /* Logs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D30E5A2A40BF540020C553 /* Logs.swift */; }; + D2D30E5C2A40BF540020C553 /* Logs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D30E5A2A40BF540020C553 /* Logs.swift */; }; + D2D30E602A40CD310020C553 /* LogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D30E5D2A40CD2C0020C553 /* LogsTests.swift */; }; + D2D30E612A40CD310020C553 /* LogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D30E5D2A40CD2C0020C553 /* LogsTests.swift */; }; + D2D3199729E982A30004F169 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; }; + D2D3199829E982AC0004F169 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */; }; + D2D3199A29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D3199929E98D970004F169 /* DefaultJSONEncoder.swift */; }; + D2D3199B29E98D970004F169 /* DefaultJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D3199929E98D970004F169 /* DefaultJSONEncoder.swift */; }; + D2D36DCB2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D36DCA2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift */; }; + D2D36DCC2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D36DCA2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift */; }; + D2DA2358298D57AA00C6C7E6 /* CoreLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CF298D5235001A1FA3 /* CoreLogger.swift */; }; + D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B9298D5235001A1FA3 /* NetworkConnectionInfo.swift */; }; + D2DA235A298D57AA00C6C7E6 /* TrackingConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BB298D5235001A1FA3 /* TrackingConsent.swift */; }; + D2DA235B298D57AA00C6C7E6 /* DynamicCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C7298D5235001A1FA3 /* DynamicCodingKey.swift */; }; + D2DA235C298D57AA00C6C7E6 /* FeatureRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D5298D5235001A1FA3 /* FeatureRequestBuilder.swift */; }; + D2DA235D298D57AA00C6C7E6 /* AttributesSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CC298D5235001A1FA3 /* AttributesSanitizer.swift */; }; + D2DA235E298D57AA00C6C7E6 /* DatadogFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BD298D5235001A1FA3 /* DatadogFeature.swift */; }; + D2DA235F298D57AA00C6C7E6 /* CarrierInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B6298D5235001A1FA3 /* CarrierInfo.swift */; }; + D2DA2360298D57AA00C6C7E6 /* DDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039DC298D5235001A1FA3 /* DDError.swift */; }; + D2DA2361298D57AA00C6C7E6 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C8298D5235001A1FA3 /* AnyCodable.swift */; }; + D2DA2363298D57AA00C6C7E6 /* BatteryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B5298D5235001A1FA3 /* BatteryStatus.swift */; }; + D2DA2364298D57AA00C6C7E6 /* LaunchTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BE298D5235001A1FA3 /* LaunchTime.swift */; }; + D2DA2365298D57AA00C6C7E6 /* FeatureMessageReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C1298D5235001A1FA3 /* FeatureMessageReceiver.swift */; }; + D2DA2366298D57AA00C6C7E6 /* Writer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039AF298D5235001A1FA3 /* Writer.swift */; }; + D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D0298D5235001A1FA3 /* Telemetry.swift */; }; + D2DA2368298D57AA00C6C7E6 /* DataFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D3298D5235001A1FA3 /* DataFormat.swift */; }; + D2DA2369298D57AA00C6C7E6 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C9298D5235001A1FA3 /* AnyEncodable.swift */; }; + D2DA236A298D57AA00C6C7E6 /* DatadogExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D8298D5235001A1FA3 /* DatadogExtended.swift */; }; + D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B8298D5235001A1FA3 /* Sysctl.swift */; }; + D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B3298D5235001A1FA3 /* AppState.swift */; }; + D2DA236D298D57AA00C6C7E6 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BC298D5235001A1FA3 /* DeviceInfo.swift */; }; + D2DA236E298D57AA00C6C7E6 /* InternalLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CE298D5235001A1FA3 /* InternalLogger.swift */; }; + D2DA236F298D57AA00C6C7E6 /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D9298D5235001A1FA3 /* DateFormatting.swift */; }; + D2DA2370298D57AA00C6C7E6 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C5298D5235001A1FA3 /* AnyDecodable.swift */; }; + D2DA2372298D57AA00C6C7E6 /* DD.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039AD298D5234001A1FA3 /* DD.swift */; }; + D2DA2373298D57AA00C6C7E6 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039DB298D5235001A1FA3 /* ReadWriteLock.swift */; }; + D2DA2374298D57AA00C6C7E6 /* DatadogContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039BA298D5235001A1FA3 /* DatadogContext.swift */; }; + D2DA2375298D57AA00C6C7E6 /* Foundation+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */; }; + D2DA2376298D57AA00C6C7E6 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B4298D5235001A1FA3 /* UserInfo.swift */; }; + D2DA2377298D57AA00C6C7E6 /* URLRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D2298D5235001A1FA3 /* URLRequestBuilder.swift */; }; + D2DA2378298D57AA00C6C7E6 /* Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039CB298D5235001A1FA3 /* Attributes.swift */; }; + D2DA2379298D57AA00C6C7E6 /* AnyDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C6298D5235001A1FA3 /* AnyDecoder.swift */; }; + D2DA237A298D57AA00C6C7E6 /* FeatureMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C2298D5235001A1FA3 /* FeatureMessage.swift */; }; + D2DA237B298D57AA00C6C7E6 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B7298D5235001A1FA3 /* DateProvider.swift */; }; + D2DA237C298D57AA00C6C7E6 /* DatadogCoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */; }; + D2DA237D298D57AA00C6C7E6 /* DataCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039D4298D5235001A1FA3 /* DataCompression.swift */; }; + D2DA237E298D57AA00C6C7E6 /* AnyEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23039C4298D5235001A1FA3 /* AnyEncoder.swift */; }; + D2DA238E298D588A00C6C7E6 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; platformFilter = ios; }; + D2DA23A1298D58F400C6C7E6 /* ReadWriteLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2395298D58F300C6C7E6 /* ReadWriteLockTests.swift */; }; + D2DA23A3298D58F400C6C7E6 /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2398298D58F300C6C7E6 /* AnyEncodableTests.swift */; }; + D2DA23A4298D58F400C6C7E6 /* AnyCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2399298D58F300C6C7E6 /* AnyCodableTests.swift */; }; + D2DA23A5298D58F400C6C7E6 /* AnyDecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239A298D58F300C6C7E6 /* AnyDecodableTests.swift */; }; + D2DA23A6298D58F400C6C7E6 /* AnyCoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239B298D58F300C6C7E6 /* AnyCoderTests.swift */; }; + D2DA23A7298D58F400C6C7E6 /* AppStateHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239D298D58F300C6C7E6 /* AppStateHistoryTests.swift */; }; + D2DA23A8298D58F400C6C7E6 /* DeviceInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239E298D58F300C6C7E6 /* DeviceInfoTests.swift */; }; + D2DA23AA298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23A0298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift */; }; + D2DA23AB298D595100C6C7E6 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; platformFilter = ios; }; + D2DA23B1298D59DC00C6C7E6 /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2398298D58F300C6C7E6 /* AnyEncodableTests.swift */; }; + D2DA23B2298D59DC00C6C7E6 /* AppStateHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239D298D58F300C6C7E6 /* AppStateHistoryTests.swift */; }; + D2DA23B3298D59DC00C6C7E6 /* AnyDecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239A298D58F300C6C7E6 /* AnyDecodableTests.swift */; }; + D2DA23B4298D59DC00C6C7E6 /* AnyCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2399298D58F300C6C7E6 /* AnyCodableTests.swift */; }; + D2DA23B5298D59DC00C6C7E6 /* ReadWriteLockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA2395298D58F300C6C7E6 /* ReadWriteLockTests.swift */; }; + D2DA23B6298D59DC00C6C7E6 /* AnyCoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239B298D58F300C6C7E6 /* AnyCoderTests.swift */; }; + D2DA23B8298D59DC00C6C7E6 /* FeatureMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23A0298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift */; }; + D2DA23BA298D59DC00C6C7E6 /* DeviceInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA239E298D58F300C6C7E6 /* DeviceInfoTests.swift */; }; + D2DA23C5298D59F300C6C7E6 /* TestUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; }; + D2DA23C7298D5AC000C6C7E6 /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23C6298D5AC000C6C7E6 /* TelemetryMocks.swift */; }; + D2DA23C8298D5AC000C6C7E6 /* TelemetryMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23C6298D5AC000C6C7E6 /* TelemetryMocks.swift */; }; + D2DA23CA298D5C1300C6C7E6 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23C9298D5C1300C6C7E6 /* UIKitMocks.swift */; }; + D2DA23CB298D5C1300C6C7E6 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA23C9298D5C1300C6C7E6 /* UIKitMocks.swift */; }; + D2DA23CF298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26D628A647DB005DD405 /* FeatureMessageReceiverMock.swift */; }; + D2DA23D0298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26D628A647DB005DD405 /* FeatureMessageReceiverMock.swift */; }; + D2DA23D1298D61FB00C6C7E6 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; }; + D2DC4BBD27F234E000E4FB96 /* CITestIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */; }; + D2DC4BF627F484AA00E4FB96 /* DataEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DC4BF527F484AA00E4FB96 /* DataEncryption.swift */; }; + 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 */; }; + D2EBEE2129BA160F00B15732 /* W3CHTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */; }; + D2EBEE2229BA160F00B15732 /* TracePropagationHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDD229B8A58E00B15732 /* TracePropagationHeadersReader.swift */; }; + D2EBEE2329BA160F00B15732 /* B3HTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */; }; + D2EBEE2429BA160F00B15732 /* W3CHTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */; }; + D2EBEE2529BA160F00B15732 /* TraceID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDCC29B893D800B15732 /* TraceID.swift */; }; + D2EBEE2629BA160F00B15732 /* B3HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773D32924EA2D00AC1A62 /* B3HTTPHeaders.swift */; }; + D2EBEE2729BA160F00B15732 /* B3HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */; }; + D2EBEE2829BA160F00B15732 /* W3CHTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */; }; + D2EBEE2929BA160F00B15732 /* HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */; }; + D2EBEE2A29BA160F00B15732 /* TracingHTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E13B02524B8F80098C6B0 /* TracingHTTPHeaders.swift */; }; + D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E13A92524B8700098C6B0 /* HTTPHeadersReader.swift */; }; + D2EBEE2E29BA161100B15732 /* TracePropagationHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDCF29B8A02100B15732 /* TracePropagationHeadersWriter.swift */; }; + D2EBEE2F29BA161100B15732 /* W3CHTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */; }; + D2EBEE3029BA161100B15732 /* TracePropagationHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDD229B8A58E00B15732 /* TracePropagationHeadersReader.swift */; }; + D2EBEE3129BA161100B15732 /* B3HTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */; }; + D2EBEE3229BA161100B15732 /* W3CHTTPHeadersReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */; }; + D2EBEE3329BA161100B15732 /* TraceID.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEDCC29B893D800B15732 /* TraceID.swift */; }; + D2EBEE3429BA161100B15732 /* B3HTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773D32924EA2D00AC1A62 /* B3HTTPHeaders.swift */; }; + D2EBEE3529BA161100B15732 /* B3HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */; }; + D2EBEE3629BA161100B15732 /* W3CHTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */; }; + D2EBEE3729BA161100B15732 /* HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */; }; + D2EBEE3829BA161100B15732 /* TracingHTTPHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E13B02524B8F80098C6B0 /* TracingHTTPHeaders.swift */; }; + D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */; }; + D2EBEE3C29BA163E00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */; }; + D2EBEE3D29BA163E00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */; }; + D2EBEE3E29BA163E00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */; }; + D2EBEE3F29BA163F00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */; }; + D2EBEE4029BA163F00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */; }; + D2EBEE4129BA163F00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */; }; + D2EBEE4229BA163F00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */; }; + D2EBEE4329BA168200B15732 /* TraceIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */; }; + D2EBEE4429BA168200B15732 /* TraceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */; }; + D2EBEE4529BA168400B15732 /* TraceIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */; }; + D2EBEE4629BA168400B15732 /* TraceIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */; }; + D2EBEE4829BA17C400B15732 /* NetworkInstrumentationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */; }; + D2EBEE4929BA17C400B15732 /* NetworkInstrumentationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */; }; + D2EFA868286DA85700F1FAA6 /* DatadogContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFA867286DA85700F1FAA6 /* DatadogContextProvider.swift */; }; + D2EFA869286DA85700F1FAA6 /* DatadogContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFA867286DA85700F1FAA6 /* DatadogContextProvider.swift */; }; + D2EFA875286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFA874286E011900F1FAA6 /* DatadogContextProviderTests.swift */; }; + D2EFA876286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFA874286E011900F1FAA6 /* DatadogContextProviderTests.swift */; }; + D2F44FB8299AA1DA0074B0D9 /* DataCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213532F270CA722000315AD /* DataCompressionTests.swift */; }; + D2F44FB9299AA1DB0074B0D9 /* DataCompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213532F270CA722000315AD /* DataCompressionTests.swift */; }; + D2F44FBC299AA36D0074B0D9 /* Decompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F44FBB299AA36D0074B0D9 /* Decompression.swift */; }; + D2F44FBD299AA36D0074B0D9 /* Decompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F44FBB299AA36D0074B0D9 /* Decompression.swift */; }; + D2F44FC2299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F44FC1299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift */; }; + D2F44FC3299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F44FC1299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift */; }; + D2F8235329915E12003C7E99 /* DatadogSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F8235229915E12003C7E99 /* DatadogSite.swift */; }; + D2F8235429915E12003C7E99 /* DatadogSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F8235229915E12003C7E99 /* DatadogSite.swift */; }; + D2FB1254292E0E96005B13F8 /* TrackingConsentPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB1253292E0E92005B13F8 /* TrackingConsentPublisher.swift */; }; + D2FB1255292E0E99005B13F8 /* TrackingConsentPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB1253292E0E92005B13F8 /* TrackingConsentPublisher.swift */; }; + D2FB1257292E0F0E005B13F8 /* TrackingConsentPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB1256292E0F0B005B13F8 /* TrackingConsentPublisherTests.swift */; }; + D2FB1258292E0F10005B13F8 /* TrackingConsentPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB1256292E0F0B005B13F8 /* TrackingConsentPublisherTests.swift */; }; + D2FB125D292FBB56005B13F8 /* Datadog+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */; }; + D2FB125E292FBB56005B13F8 /* Datadog+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */; }; + E143CCAF27D236F600F4018A /* CITestIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */; }; + 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 */ + 3C41693D29FBF5BB0042B9D2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D257953D298ABA65008A1BE5; + remoteInfo = "TestUtilities iOS"; + }; + 3C41693F29FBF5F20042B9D2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + 3C41694129FBF6100042B9D2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + 3C4D5FEE2A0115C600F1FF78 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2C1A53329C4F2DF00946C31; + remoteInfo = "DatadogTrace tvOS"; + }; + 3C4D5FF02A0115CB00F1FF78 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + 3C9C6BB629F7C0C000581C43 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + 3CE11A0729F7BE0500202522 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3CE119FD29F7BE0000202522; + remoteInfo = "DatadogWebViewTracking iOS"; + }; 61133C722423993200786299 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; @@ -203,19 +1813,33 @@ remoteGlobalIDString = 61133B81242393DE00786299; remoteInfo = Datadog; }; - 61441C2F24616F1D003D8BB8 /* PBXContainerItemProxy */ = { + 6133D1E72A6ED9E100384BEF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; - remoteGlobalIDString = 61441C0124616DE9003D8BB8; - remoteInfo = Example; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; }; - 61441C4F24619499003D8BB8 /* PBXContainerItemProxy */ = { + 6133D1F82A6EDB7700384BEF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; - remoteGlobalIDString = 61133B81242393DE00786299; - remoteInfo = Datadog; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + 6133D1FA2A6EDB7700384BEF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D257953D298ABA65008A1BE5; + remoteInfo = "TestUtilities iOS"; + }; + 6133D2092A6EDBAE00384BEF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6133D1E52A6ED9E100384BEF; + remoteInfo = "DatadogSessionReplay iOS"; }; 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -224,54 +1848,518 @@ remoteGlobalIDString = 61441C0124616DE9003D8BB8; remoteInfo = Example; }; - 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */ = { + 6158155A2AB4534F002C60D7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; remoteGlobalIDString = 61441C0124616DE9003D8BB8; - remoteInfo = Example; + remoteInfo = "Example iOS"; + }; + 618F9845265BC486009959F8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6199362A265BA958009D7EA8; + remoteInfo = E2E; + }; + 61993658265BB6A6009D7EA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61133B81242393DE00786299; + remoteInfo = Datadog; + }; + 6199365C265BB6A6009D7EA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61B7885325C180CB002675B5; + remoteInfo = DatadogCrashReporting; + }; + 6199366A265BBEDC009D7EA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6199362A265BA958009D7EA8; + remoteInfo = E2E; + }; + 61A2CC282A4449210000FF25 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D29A9F3329DD84AA005C54A4; + remoteInfo = "DatadogRUM iOS"; + }; + 61A2CC2D2A4449300000FF25 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23F8E4D29DDCD28001CFAE8; + remoteInfo = "DatadogRUM tvOS"; + }; + 61B7885E25C180CB002675B5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61B7885325C180CB002675B5; + remoteInfo = DatadogCrashReporting; + }; + D206BB872A41CA6800F43BA2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D207317B29A5226A00ECBF94; + remoteInfo = "DatadogLogs iOS"; + }; + D206BB8C2A41CA7000F43BA2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D20731A429A5279D00ECBF94; + remoteInfo = "DatadogLogs tvOS"; + }; + D207318529A5226B00ECBF94 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D207317B29A5226A00ECBF94; + remoteInfo = DatadogLogs; + }; + D207319929A5232A00ECBF94 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D22A031A29F7DAA9002C02C6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D20731A429A5279D00ECBF94; + remoteInfo = "DatadogLogs tvOS"; + }; + D22A031C29F7DABE002C02C6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23F8E4D29DDCD28001CFAE8; + remoteInfo = "DatadogRUM tvOS"; + }; + D2303A06298D5317001A1FA3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D231F7AF2A00FF28000D6239 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D231F7B12A00FF2F000D6239 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + D231F7B32A00FF8F000D6239 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61133B81242393DE00786299; + remoteInfo = "Datadog iOS"; + }; + D231F7B52A00FF9A000D6239 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61B7885325C180CB002675B5; + remoteInfo = "DatadogCrashReporting iOS"; + }; + D231F7B72A00FFA3000D6239 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2CB6FBA27C5348200A62B57; + remoteInfo = "DatadogCrashReporting tvOS"; + }; + D23F8ECF29DDCD5C001CFAE8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + D240685627CF5D0100C04F44 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2CB6E0A27C50EAE00A62B57; + remoteInfo = "Datadog tvOS"; + }; + D240686C27CF687200C04F44 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D24067F827CE6C9E00C04F44; + remoteInfo = "Example tvOS"; + }; + D25CFA9A29C4F41F00E3A43D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2C1A53329C4F2DF00946C31; + remoteInfo = "DatadogTrace tvOS"; + }; + D25EE93D29C4C3C300CE3839 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D25EE93329C4C3C300CE3839; + remoteInfo = DatadogTrace; + }; + D26F741429ACBDAD00D25622 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + D28D5D5327C53A60008E72D0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2CB6FBA27C5348200A62B57; + remoteInfo = "DatadogCrashReporting tvOS"; + }; + D29A9F3D29DD84AB005C54A4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D29A9F3329DD84AA005C54A4; + remoteInfo = DatadogRUM; + }; + D29A9F4D29DD8525005C54A4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D2A434A42A8E3F900028E329 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6133D1E52A6ED9E100384BEF; + remoteInfo = "DatadogSessionReplay iOS"; + }; + D2A783E229A53414003B03BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + D2C1A51029C4C4EF00946C31 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D2C1A52B29C4C92800946C31 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D25EE93329C4C3C300CE3839; + remoteInfo = "DatadogTrace iOS"; + }; + D2C1A57629C4F30000946C31 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; + }; + D2CB6FB527C5234300A62B57 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2CB6E0A27C50EAE00A62B57; + remoteInfo = "Datadog tvOS"; + }; + D2DA238F298D588A00C6C7E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D23039A4298D513C001A1FA3; + remoteInfo = "DatadogInternal iOS"; + }; + D2DA23D2298D620F00C6C7E6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D2DA2355298D57AA00C6C7E6; + remoteInfo = "DatadogInternal tvOS"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 61133C742423993200786299 /* Embed Frameworks */ = { + 61993660265BB6A6009D7EA8 /* ⚙️ Embed Framework Dependencies */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 61133C712423993200786299 /* Datadog.framework in Embed Frameworks */, + 61993657265BB6A6009D7EA8 /* DatadogCore.framework in ⚙️ Embed Framework Dependencies */, + 6199365B265BB6A6009D7EA8 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */, ); - name = "Embed Frameworks"; + name = "⚙️ Embed Framework Dependencies"; runOnlyForDeploymentPostprocessing = 0; }; - 61441C5124619499003D8BB8 /* ⚙️ Embed Framework Dependencies */ = { + D240684527CE6C9E00C04F44 /* ⚙️ Embed Framework Dependencies */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 61441C4E24619498003D8BB8 /* Datadog.framework in ⚙️ Embed Framework Dependencies */, - 61570006246AAE5E00E96950 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */, + D240685927CF5D0100C04F44 /* DatadogCrashReporting.framework in ⚙️ Embed Framework Dependencies */, + D240685527CF5D0100C04F44 /* DatadogCore.framework in ⚙️ Embed Framework Dependencies */, + 1434A4642B7F73170072E3BB /* OpenTelemetryApi.xcframework in ⚙️ Embed Framework Dependencies */, + D24C9C4729A7A520002057CF /* DatadogLogs.framework in ⚙️ Embed Framework Dependencies */, + D240686027CF5D0100C04F44 /* DatadogObjc.framework in ⚙️ Embed Framework Dependencies */, ); name = "⚙️ Embed Framework Dependencies"; runOnlyForDeploymentPostprocessing = 0; }; + D240687A27CF982B00C04F44 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3C2206F82AB9DBC600DE780C /* DatadogInternal.framework in Embed Frameworks */, + 3C2206F72AB9DBB600DE780C /* DatadogTrace.framework in Embed Frameworks */, + 1434A4622B7F73110072E3BB /* OpenTelemetryApi.xcframework in Embed Frameworks */, + 3C2206F62AB9DBA700DE780C /* DatadogRUM.framework in Embed Frameworks */, + 3C2206F52AB9DB9000DE780C /* DatadogSessionReplay.framework in Embed Frameworks */, + D240687E27CF982D00C04F44 /* DatadogCrashReporting.framework in Embed Frameworks */, + D240687C27CF982C00C04F44 /* DatadogCore.framework in Embed Frameworks */, + D24C9C4329A7A50D002057CF /* DatadogLogs.framework in Embed Frameworks */, + 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */, + D240688027CF982F00C04F44 /* DatadogObjc.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 61133B82242393DE00786299 /* Datadog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Datadog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 61133B85242393DE00786299 /* Datadog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Datadog.h; sourceTree = ""; }; + 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 = ""; }; + 3C0D5DE62A543E9700446CF9 /* RUMViewEventsFilterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMViewEventsFilterTests.swift; sourceTree = ""; }; + 3C0D5DEB2A54405A00446CF9 /* RUMViewEventsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewEventsFilter.swift; sourceTree = ""; }; + 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMocks.swift; sourceTree = ""; }; + 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFormatTests.swift; sourceTree = ""; }; + 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDURLSessionInstrumentationTests+apiTests.m"; sourceTree = ""; }; + 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = OpenTelemetryApi.xcframework; path = ../Carthage/Build/OpenTelemetryApi.xcframework; sourceTree = ""; }; + 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 = ""; }; + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+Datadog.swift"; sourceTree = ""; }; + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelSpanId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceId+DatadogTests.swift"; sourceTree = ""; }; + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpanTests.swift; sourceTree = ""; }; + 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 = ""; }; + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NOPOTelSpanBuilder.swift; sourceTree = ""; }; + 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionInstrumentation.swift; sourceTree = ""; }; + 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionTask+Tracking.swift"; sourceTree = ""; }; + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+Datadog.swift"; sourceTree = ""; }; + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelAttributeValue+DatadogTests.swift"; sourceTree = ""; }; + 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionInstrumentation+objc.swift"; sourceTree = ""; }; + 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionInstrumentationConfigurationTests.swift; sourceTree = ""; }; + 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDGeneratorTests.swift; sourceTree = ""; }; + 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 = ""; }; + 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUM+Internal.swift"; sourceTree = ""; }; + 49D8C0B92AC5F21F0075E427 /* Logs+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logs+Internal.swift"; sourceTree = ""; }; + 61020C292757AD91005EEAEA /* BackgroundLocationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundLocationMonitor.swift; sourceTree = ""; }; + 61020C2B2758E853005EEAEA /* DebugBackgroundEventsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugBackgroundEventsViewController.swift; sourceTree = ""; }; + 61054E082A6EE10A00AAA894 /* SRCompression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRCompression.swift; sourceTree = ""; }; + 61054E092A6EE10A00AAA894 /* RecordWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWriter.swift; sourceTree = ""; }; + 61054E0B2A6EE10A00AAA894 /* SessionReplayConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayConfiguration.swift; sourceTree = ""; }; + 61054E0C2A6EE10A00AAA894 /* SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplay.swift; sourceTree = ""; }; + 61054E0F2A6EE10A00AAA894 /* AppWindowObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppWindowObserver.swift; sourceTree = ""; }; + 61054E102A6EE10A00AAA894 /* KeyWindowObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyWindowObserver.swift; sourceTree = ""; }; + 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 /* 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+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 = ""; }; + 61054E1E2A6EE10A00AAA894 /* TouchSnapshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchSnapshot.swift; sourceTree = ""; }; + 61054E1F2A6EE10A00AAA894 /* TouchIdentifierGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchIdentifierGenerator.swift; sourceTree = ""; }; + 61054E202A6EE10A00AAA894 /* WindowTouchSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowTouchSnapshotProducer.swift; sourceTree = ""; }; + 61054E222A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeSnapshotProducer.swift; sourceTree = ""; }; + 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeSnapshot.swift; sourceTree = ""; }; + 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeSnapshotBuilder.swift; sourceTree = ""; }; + 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecorder.swift; sourceTree = ""; }; + 61054E282A6EE10A00AAA894 /* UIDatePickerRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDatePickerRecorder.swift; sourceTree = ""; }; + 61054E292A6EE10A00AAA894 /* UITextViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextViewRecorder.swift; sourceTree = ""; }; + 61054E2A2A6EE10A00AAA894 /* UIImageViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewRecorder.swift; sourceTree = ""; }; + 61054E2B2A6EE10A00AAA894 /* UIViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewRecorder.swift; sourceTree = ""; }; + 61054E2C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UINavigationBarRecorder.swift; sourceTree = ""; }; + 61054E2D2A6EE10A00AAA894 /* UITextFieldRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldRecorder.swift; sourceTree = ""; }; + 61054E2E2A6EE10A00AAA894 /* NodeRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeRecorder.swift; sourceTree = ""; }; + 61054E2F2A6EE10A00AAA894 /* UISliderRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISliderRecorder.swift; sourceTree = ""; }; + 61054E302A6EE10A00AAA894 /* UIPickerViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIPickerViewRecorder.swift; sourceTree = ""; }; + 61054E312A6EE10A00AAA894 /* UIStepperRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStepperRecorder.swift; sourceTree = ""; }; + 61054E322A6EE10A00AAA894 /* UILabelRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UILabelRecorder.swift; sourceTree = ""; }; + 61054E332A6EE10A00AAA894 /* UISwitchRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISwitchRecorder.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 61054E3C2A6EE10A00AAA894 /* SessionReplayFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayFeature.swift; sourceTree = ""; }; + 61054E3E2A6EE10A00AAA894 /* RUMContextReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextReceiver.swift; sourceTree = ""; }; + 61054E3F2A6EE10A00AAA894 /* SRContextPublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRContextPublisher.swift; sourceTree = ""; }; + 61054E412A6EE10A00AAA894 /* SegmentRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentRequestBuilder.swift; sourceTree = ""; }; + 61054E432A6EE10A00AAA894 /* SegmentJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentJSON.swift; sourceTree = ""; }; + 61054E472A6EE10A00AAA894 /* MultipartFormData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = ""; }; + 61054E4A2A6EE10A00AAA894 /* TextObfuscator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextObfuscator.swift; sourceTree = ""; }; + 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessor.swift; sourceTree = ""; }; + 61054E4D2A6EE10A00AAA894 /* Diff+SRWireframes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Diff+SRWireframes.swift"; sourceTree = ""; }; + 61054E4E2A6EE10A00AAA894 /* Diff.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diff.swift; sourceTree = ""; }; + 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordsBuilder.swift; sourceTree = ""; }; + 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 = ""; }; + 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+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 = ""; }; + 61054F442A6EE1B900AAA894 /* QueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueTests.swift; sourceTree = ""; }; + 61054F452A6EE1B900AAA894 /* SwiftExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensionsTests.swift; sourceTree = ""; }; + 61054F472A6EE1B900AAA894 /* MainThreadSchedulerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainThreadSchedulerTests.swift; sourceTree = ""; }; + 61054F482A6EE1B900AAA894 /* SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionReplayTests.swift; sourceTree = ""; }; + 61054F4A2A6EE1BA00AAA894 /* RecordsWriterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordsWriterTests.swift; sourceTree = ""; }; + 61054F4B2A6EE1BA00AAA894 /* SRCompressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRCompressionTests.swift; sourceTree = ""; }; + 61054F502A6EE1BA00AAA894 /* TextObfuscatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextObfuscatorTests.swift; sourceTree = ""; }; + 61054F522A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Diff+SRWireframesTests.swift"; sourceTree = ""; }; + 61054F532A6EE1BA00AAA894 /* DiffTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffTests.swift; sourceTree = ""; }; + 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordsBuilderTests.swift; sourceTree = ""; }; + 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 /* 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 = ""; }; + 61054F672A6EE1BA00AAA894 /* ViewTreeSnapshotBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeSnapshotBuilderTests.swift; sourceTree = ""; }; + 61054F682A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeIDGeneratorTests.swift; sourceTree = ""; }; + 61054F6A2A6EE1BA00AAA894 /* UILabelRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UILabelRecorderTests.swift; sourceTree = ""; }; + 61054F6B2A6EE1BA00AAA894 /* UITextFieldRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldRecorderTests.swift; sourceTree = ""; }; + 61054F6C2A6EE1BA00AAA894 /* UITabBarRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITabBarRecorderTests.swift; sourceTree = ""; }; + 61054F6D2A6EE1BA00AAA894 /* UISliderRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISliderRecorderTests.swift; sourceTree = ""; }; + 61054F6E2A6EE1BA00AAA894 /* UnsupportedViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsupportedViewRecorderTests.swift; sourceTree = ""; }; + 61054F6F2A6EE1BA00AAA894 /* UISegmentRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISegmentRecorderTests.swift; sourceTree = ""; }; + 61054F702A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDatePickerRecorderTests.swift; sourceTree = ""; }; + 61054F712A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UINavigationBarRecorderTests.swift; sourceTree = ""; }; + 61054F722A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewRecorderTests.swift; sourceTree = ""; }; + 61054F732A6EE1BA00AAA894 /* UISwitchRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISwitchRecorderTests.swift; sourceTree = ""; }; + 61054F742A6EE1BA00AAA894 /* UIStepperRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIStepperRecorderTests.swift; sourceTree = ""; }; + 61054F752A6EE1BA00AAA894 /* UIViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewRecorderTests.swift; sourceTree = ""; }; + 61054F762A6EE1BA00AAA894 /* UIImageViewWireframesBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewWireframesBuilderTests.swift; sourceTree = ""; }; + 61054F772A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIPickerViewRecorderTests.swift; sourceTree = ""; }; + 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 /* 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 = ""; }; + 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRDataModelsMocks.swift; sourceTree = ""; }; + 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProcessorSpy.swift; sourceTree = ""; }; + 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecorderMocks.swift; sourceTree = ""; }; + 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestScheduler.swift; sourceTree = ""; }; + 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueMocks.swift; sourceTree = ""; }; + 61054F862A6EE1BA00AAA894 /* SnapshotProducerMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotProducerMocks.swift; sourceTree = ""; }; + 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextObserverMock.swift; sourceTree = ""; }; + 61054F892A6EE1BA00AAA894 /* RUMContextReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextReceiverTests.swift; sourceTree = ""; }; + 61054F8A2A6EE1BA00AAA894 /* SRContextPublisherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRContextPublisherTests.swift; sourceTree = ""; }; + 61054F902A6EE1BA00AAA894 /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; + 61054F912A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentRequestBuilderTests.swift; sourceTree = ""; }; + 61054F932A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertRectsEqual.swift; sourceTree = ""; }; + 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreTelemetryIntegrationTests.swift; sourceTree = ""; }; + 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDRUM+apiTests.m"; sourceTree = ""; }; + 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objc.swift"; sourceTree = ""; }; + 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanSanitizer.swift; sourceTree = ""; }; + 61122ED325B1B84D00F9C7F5 /* RUMEventSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventSanitizer.swift; sourceTree = ""; }; + 61122EE725B1C92500F9C7F5 /* SpanSanitizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanSanitizerTests.swift; sourceTree = ""; }; + 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventSanitizerTests.swift; sourceTree = ""; }; + 6112B11325C84E7900B37771 /* CrashReportSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportSender.swift; sourceTree = ""; }; + 61133B82242393DE00786299 /* DatadogCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 61133B85242393DE00786299 /* DatadogCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DatadogCore.h; sourceTree = ""; }; 61133B86242393DE00786299 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61133B8B242393DE00786299 /* DatadogTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 61133B8B242393DE00786299 /* DatadogCoreTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogCoreTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 61133B92242393DE00786299 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61133BA02423979B00786299 /* EncodableValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodableValue.swift; sourceTree = ""; }; - 61133BA22423979B00786299 /* CarrierInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarrierInfoProvider.swift; sourceTree = ""; }; - 61133BA32423979B00786299 /* MobileDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileDevice.swift; sourceTree = ""; }; - 61133BA42423979B00786299 /* NetworkConnectionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoProvider.swift; sourceTree = ""; }; - 61133BA52423979B00786299 /* BatteryStatusProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryStatusProvider.swift; sourceTree = ""; }; 61133BA72423979B00786299 /* FileWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileWriter.swift; sourceTree = ""; }; - 61133BA82423979B00786299 /* DateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; 61133BA92423979B00786299 /* FilesOrchestrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesOrchestrator.swift; sourceTree = ""; }; 61133BAB2423979B00786299 /* Directory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Directory.swift; sourceTree = ""; }; 61133BAC2423979B00786299 /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; @@ -279,41 +2367,23 @@ 61133BAF2423979B00786299 /* DataUploadConditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadConditions.swift; sourceTree = ""; }; 61133BB02423979B00786299 /* DataUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploader.swift; sourceTree = ""; }; 61133BB12423979B00786299 /* DataUploadWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadWorker.swift; sourceTree = ""; }; - 61133BB22423979B00786299 /* HTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + 61133BB22423979B00786299 /* URLSessionClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionClient.swift; sourceTree = ""; }; 61133BB32423979B00786299 /* DataUploadDelay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadDelay.swift; sourceTree = ""; }; - 61133BB42423979B00786299 /* HTTPHeaders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeaders.swift; sourceTree = ""; }; - 61133BB52423979B00786299 /* DatadogConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogConfiguration.swift; sourceTree = ""; }; - 61133BB62423979B00786299 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - 61133BB82423979B00786299 /* InternalLoggers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalLoggers.swift; sourceTree = ""; }; - 61133BB92423979B00786299 /* CompilationConditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompilationConditions.swift; sourceTree = ""; }; 61133BBA2423979B00786299 /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; - 61133BBB2423979B00786299 /* Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = ""; }; - 61133BC02423979B00786299 /* UserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; - 61133BC22423979B00786299 /* LogEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEncoder.swift; sourceTree = ""; }; - 61133BC32423979B00786299 /* LogBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogBuilder.swift; sourceTree = ""; }; - 61133BC42423979B00786299 /* LogSanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogSanitizer.swift; sourceTree = ""; }; - 61133BC62423979B00786299 /* LogUtilityOutputs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogUtilityOutputs.swift; sourceTree = ""; }; - 61133BC72423979B00786299 /* LogFileOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogFileOutput.swift; sourceTree = ""; }; - 61133BC82423979B00786299 /* LogOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogOutput.swift; sourceTree = ""; }; - 61133BC92423979B00786299 /* LogConsoleOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogConsoleOutput.swift; sourceTree = ""; }; + 61133BC22423979B00786299 /* LogEventEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventEncoder.swift; sourceTree = ""; }; + 61133BC32423979B00786299 /* LogEventBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventBuilder.swift; sourceTree = ""; }; + 61133BC42423979B00786299 /* LogEventSanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventSanitizer.swift; sourceTree = ""; }; 61133BF0242397DA00786299 /* DatadogObjc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogObjc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; - 61133C0C2423983800786299 /* Logger+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+objc.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 = ""; }; - 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLoggerBuilderTests.swift; sourceTree = ""; }; 61133C162423990D00786299 /* DDConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDConfigurationTests.swift; sourceTree = ""; }; - 61133C172423990D00786299 /* DDLoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLoggerTests.swift; sourceTree = ""; }; + 61133C172423990D00786299 /* DDLogsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLogsTests.swift; sourceTree = ""; }; 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreTelephonyMocks.swift; sourceTree = ""; }; 61133C1C2423990D00786299 /* UIKitMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; - 61133C202423990D00786299 /* FoundationMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationMocks.swift; sourceTree = ""; }; - 61133C232423990D00786299 /* MobileDeviceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileDeviceTests.swift; sourceTree = ""; }; - 61133C242423990D00786299 /* NetworkConnectionInfoProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoProviderTests.swift; sourceTree = ""; }; - 61133C252423990D00786299 /* BatteryStatusProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryStatusProviderTests.swift; sourceTree = ""; }; - 61133C262423990D00786299 /* CarrierInfoProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarrierInfoProviderTests.swift; sourceTree = ""; }; 61133C282423990D00786299 /* FileReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileReaderTests.swift; sourceTree = ""; }; 61133C292423990D00786299 /* FileWriterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileWriterTests.swift; sourceTree = ""; }; 61133C2A2423990D00786299 /* FilesOrchestratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesOrchestratorTests.swift; sourceTree = ""; }; @@ -321,145 +2391,803 @@ 61133C2D2423990D00786299 /* DirectoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryTests.swift; sourceTree = ""; }; 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadWorkerTests.swift; sourceTree = ""; }; 61133C302423990D00786299 /* DataUploadConditionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadConditionsTests.swift; sourceTree = ""; }; - 61133C312423990D00786299 /* LogsUploadDelayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogsUploadDelayTests.swift; sourceTree = ""; }; + 61133C312423990D00786299 /* DataUploadDelayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploadDelayTests.swift; sourceTree = ""; }; 61133C322423990D00786299 /* DataUploaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataUploaderTests.swift; sourceTree = ""; }; - 61133C332423990D00786299 /* HTTPHeadersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeadersTests.swift; sourceTree = ""; }; - 61133C342423990D00786299 /* HTTPClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClientTests.swift; sourceTree = ""; }; - 61133C362423990D00786299 /* InternalLoggersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalLoggersTests.swift; sourceTree = ""; }; + 61133C332423990D00786299 /* RequestBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBuilderTests.swift; sourceTree = ""; }; + 61133C342423990D00786299 /* URLSessionClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionClientTests.swift; sourceTree = ""; }; 61133C382423990D00786299 /* LoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; - 61133C3B2423990D00786299 /* LogBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogBuilderTests.swift; sourceTree = ""; }; + 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogEventBuilderTests.swift; sourceTree = ""; }; 61133C3C2423990D00786299 /* LogSanitizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogSanitizerTests.swift; sourceTree = ""; }; - 61133C3E2423990D00786299 /* LogConsoleOutputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogConsoleOutputTests.swift; sourceTree = ""; }; - 61133C3F2423990D00786299 /* LogUtilityOutputsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogUtilityOutputsTests.swift; sourceTree = ""; }; - 61133C402423990D00786299 /* LogFileOutputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogFileOutputTests.swift; sourceTree = ""; }; 61133C412423990D00786299 /* DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogTests.swift; sourceTree = ""; }; 61133C432423990D00786299 /* LogMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogMatcher.swift; sourceTree = ""; }; - 61133C452423990D00786299 /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; 61133C462423990D00786299 /* TestsDirectory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestsDirectory.swift; sourceTree = ""; }; 61133C472423990D00786299 /* DatadogExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogExtensions.swift; sourceTree = ""; }; - 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingForTracingAdapter.swift; sourceTree = ""; }; - 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingForTracingAdapterTests.swift; sourceTree = ""; }; - 612983CC2449E62E00D4424B /* LoggingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeature.swift; sourceTree = ""; }; + 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExtensionsTests.swift; sourceTree = ""; }; + 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 = ""; }; + 61216B752666DDA10089DCD1 /* LoggerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerConfigurationTests.swift; sourceTree = ""; }; + 61216B7826679DD20089DCD1 /* RUME2EHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUME2EHelpers.swift; sourceTree = ""; }; + 61216B7A2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsConfigurationE2ETests.swift; sourceTree = ""; }; + 61216B7D2667BC220089DCD1 /* DatadogE2EHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogE2EHelpers.swift; sourceTree = ""; }; + 61216B7F2667C79B0089DCD1 /* LogsTrackingConsentE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsTrackingConsentE2ETests.swift; sourceTree = ""; }; + 6122514727FDFF82004F5AE4 /* RUMScopeDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScopeDependencies.swift; sourceTree = ""; }; + 612556AF268C8D31002BCE74 /* CrashReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReport.swift; sourceTree = ""; }; + 612556BA268DD9BF002BCE74 /* DDCrashReportExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportExporter.swift; sourceTree = ""; }; + 6128F5692BA2237300D35B08 /* DataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; + 6128F56D2BA223A100D35B08 /* FeatureDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureDataStore.swift; sourceTree = ""; }; + 6128F5702BA223D100D35B08 /* DataStore+TLV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataStore+TLV.swift"; sourceTree = ""; }; + 6128F5732BA3280300D35B08 /* DataStoreFileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreFileReader.swift; sourceTree = ""; }; + 6128F5762BA32DE500D35B08 /* DataStoreFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreFileWriter.swift; sourceTree = ""; }; + 6128F57A2BA35D6200D35B08 /* FeatureDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureDataStoreTests.swift; sourceTree = ""; }; + 6128F57D2BA8A3A000D35B08 /* DataStore+TLVTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataStore+TLVTests.swift"; sourceTree = ""; }; + 6128F5832BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreFileWriterTests.swift; sourceTree = ""; }; + 6128F5892BA9860B00D35B08 /* DataStoreFileReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreFileReaderTests.swift; sourceTree = ""; }; + 612C13CF2AA772FA0086B5D1 /* SRRequestMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRRequestMatcher.swift; sourceTree = ""; }; + 612C13D22AAA20660086B5D1 /* JSONObjectMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONObjectMatcher.swift; sourceTree = ""; }; + 612C13D52AAB35EB0086B5D1 /* SRSegmentMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRSegmentMatcher.swift; sourceTree = ""; }; 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTTracer+objc.swift"; sourceTree = ""; }; - 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTGlobal+objc.swift"; sourceTree = ""; }; 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDSpan+objc.swift"; sourceTree = ""; }; 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDSpanContext+objc.swift"; sourceTree = ""; }; 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeadersWriter+objc.swift"; sourceTree = ""; }; - 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTNoop.swift; sourceTree = ""; }; 6132BF5024A49F7400D7BD17 /* Casting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; + 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogSessionReplay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6133D2082A6EDB7700384BEF /* DatadogSessionReplayTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogSessionReplayTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 61345612244756E300E7DA6B /* PerformancePresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePresetTests.swift; sourceTree = ""; }; - 61441C0224616DE9003D8BB8 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 61441C0424616DE9003D8BB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchMetricsTests.swift; sourceTree = ""; }; + 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDErrorTests.swift; sourceTree = ""; }; + 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesOrchestrator+MetricsTests.swift"; sourceTree = ""; }; + 61378BA72555329E00F28837 /* DatadogTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogTests.xcconfig; sourceTree = ""; }; + 61378BB22555337900F28837 /* DatadogSDKTesting.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogSDKTesting.local.xcconfig; sourceTree = ""; }; + 6139CD702589FAFD007E8BB7 /* Retrying.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retrying.swift; sourceTree = ""; }; + 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryingTests.swift; sourceTree = ""; }; + 613C6B8F2768FDDE00870CBF /* Sampler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sampler.swift; sourceTree = ""; }; + 613C6B912768FF3100870CBF /* SamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplerTests.swift; sourceTree = ""; }; + 613E792E2577B0F900DFCC17 /* Reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reader.swift; sourceTree = ""; }; + 613E793A2577B6EE00DFCC17 /* DataReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataReader.swift; sourceTree = ""; }; + 613E81EF25A740140084B751 /* RUMEventsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventsMapper.swift; sourceTree = ""; }; + 613E81F625A743600084B751 /* RUMEventsMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventsMapperTests.swift; sourceTree = ""; }; + 613E820425A879AF0084B751 /* RUMDataModelMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataModelMocks.swift; sourceTree = ""; }; + 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 /* 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 = ""; }; + 61441C0224616DE9003D8BB8 /* Example iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppDelegate.swift; sourceTree = ""; }; + 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Main iOS.storyboard"; sourceTree = ""; }; 61441C0D24616DEC003D8BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 61441C1024616DEC003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 61441C1224616DEC003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 61441C3B24617013003D8BB8 /* IntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; - 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingIntegrationTests.swift; sourceTree = ""; }; - 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogBenchmarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 61441C6C24619FE4003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingBenchmarkTests.swift; sourceTree = ""; }; - 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingStorageBenchmarkTests.swift; sourceTree = ""; }; 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleOutputInterceptor.swift; sourceTree = ""; }; 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Disabling.swift"; sourceTree = ""; }; - 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+KeyboardControlling.swift"; sourceTree = ""; }; 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugTracingViewController.swift; sourceTree = ""; }; 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLoggingViewController.swift; sourceTree = ""; }; - 61441C9C2461A796003D8BB8 /* AppConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; + 614798952A459AA80095CB02 /* DDTraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTraceTests.swift; sourceTree = ""; }; + 614798982A459B2E0095CB02 /* DDTraceConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTraceConfigurationTests.swift; sourceTree = ""; }; + 6147989B2A459E2B0095CB02 /* DDTrace+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDTrace+apiTests.m"; sourceTree = ""; }; + 6147E3B2270486920092BC9F /* TraceConfigurationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceConfigurationE2ETests.swift; sourceTree = ""; }; 614872762485067300E3EBDB /* SpanTagsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanTagsReducer.swift; sourceTree = ""; }; - 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; + 61494CB024C839460082C633 /* RUMResourceScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScope.swift; sourceTree = ""; }; + 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMResourceScopeTests.swift; sourceTree = ""; }; + 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUserActionScope.swift; sourceTree = ""; }; + 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmutableRequest.swift; sourceTree = ""; }; + 614B0A4A24EBC43D00A2A780 /* RUMUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUser.swift; sourceTree = ""; }; + 614B0A4E24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMConnectivityInfoProvider.swift; sourceTree = ""; }; + 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogCoreTests.swift; sourceTree = ""; }; + 614B78EC296D7B63009C6B92 /* LowPowerModePublisherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LowPowerModePublisherTests.swift; sourceTree = ""; }; + 614CADD62510BAC000B93D2D /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; + 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = ../Carthage/Build/CrashReporter.xcframework; sourceTree = ""; }; + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersWriterTests.swift; sourceTree = ""; }; + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogTracer+InjectAndExtract.swift"; sourceTree = ""; }; + 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519262461BCE7002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; - 615A4A8224A3431600233986 /* Tracer+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tracer+objc.swift"; sourceTree = ""; }; - 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TracerConfiguration+objc.swift"; sourceTree = ""; }; - 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTracerConfigurationTests.swift; sourceTree = ""; }; + 61569894256D0E9A00C6AADA /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; + 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImmutableRequestTests.swift; sourceTree = ""; }; + 6156CB8D24DDA1B5008CB2B2 /* RUMContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMContextProvider.swift; sourceTree = ""; }; + 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayDependencyTests.swift; sourceTree = ""; }; + 615950ED291C058F00470E0C /* SessionReplayDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayDependency.swift; sourceTree = ""; }; + 615A4A8224A3431600233986 /* Trace+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trace+objc.swift"; sourceTree = ""; }; 615A4A8824A34FD700233986 /* DDTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTracerTests.swift; sourceTree = ""; }; 615A4A8A24A3568900233986 /* OTSpan+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTSpan+objc.swift"; sourceTree = ""; }; 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTSpanContext+objc.swift"; sourceTree = ""; }; - 617CEB382456BC3A00AD4669 /* TracingUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUID.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 /* 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 = ""; }; + 6161249D25CAB340009901BE /* CrashContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashContext.swift; sourceTree = ""; }; + 616124A625CAC268009901BE /* CrashContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashContextProvider.swift; sourceTree = ""; }; + 6167ACBD251A0B410012B4D0 /* Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Example-Bridging-Header.h"; sourceTree = ""; }; + 6167C79226665D6900D4CF07 /* E2EUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EUtils.swift; sourceTree = ""; }; + 6167C7942666622800D4CF07 /* LoggingE2EHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingE2EHelpers.swift; sourceTree = ""; }; + 6167E6D22B7F8B3300C3CA2D /* AppHangsMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsMonitor.swift; sourceTree = ""; }; + 6167E6D52B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsWatchdogThread.swift; sourceTree = ""; }; + 6167E6D92B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsWatchdogThreadTests.swift; sourceTree = ""; }; + 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsMonitoringTests.swift; sourceTree = ""; }; + 6167E6E12B81207200C3CA2D /* DDCrashReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReport.swift; sourceTree = ""; }; + 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReport.swift; sourceTree = ""; }; + 6167E6F52B81E94C00C3CA2D /* DDThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDThread.swift; sourceTree = ""; }; + 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryImage.swift; sourceTree = ""; }; + 6167E6FC2B81EC0400C3CA2D /* BacktraceReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReporter.swift; sourceTree = ""; }; + 6167E6FF2B81EF7500C3CA2D /* BacktraceReportingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReportingFeature.swift; sourceTree = ""; }; + 6167E7022B81F2EB00C3CA2D /* BacktraceReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReporter.swift; sourceTree = ""; }; + 6167E7052B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratingBacktraceTests.swift; sourceTree = ""; }; + 6167E70D2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogCore+FeatureDirectoriesTests.swift"; sourceTree = ""; }; + 6167E7112B837F0B00C3CA2D /* BacktraceReportingMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReportingMocks.swift; sourceTree = ""; }; + 6167E7182B837F7A00C3CA2D /* BacktraceReportMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReportMocks.swift; sourceTree = ""; }; + 6167E71D2B837FB200C3CA2D /* DDThreadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDThreadMocks.swift; sourceTree = ""; }; + 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryImageMocks.swift; sourceTree = ""; }; + 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportMocks.swift; sourceTree = ""; }; + 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitHelpers.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessIdentifier.swift; sourceTree = ""; }; + 6170DC1B25C18729003AED5C /* PLCrashReporterPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLCrashReporterPlugin.swift; sourceTree = ""; }; + 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogCrashReportingTests.xcconfig; sourceTree = ""; }; + 6172472625D673D7007085B3 /* CrashContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashContextTests.swift; sourceTree = ""; }; + 617247AD25DA9BEA007085B3 /* CrashReportingObjcHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrashReportingObjcHelpers.h; sourceTree = ""; }; + 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashReportingObjcHelpers.m; sourceTree = ""; }; + 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportBuilder.swift; sourceTree = ""; }; + 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebViewTracking+objc.swift"; sourceTree = ""; }; + 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDWebViewTracking+apiTests.m"; sourceTree = ""; }; + 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKMetricFields.swift; sourceTree = ""; }; + 6174D60F2BFDEA4600EC7469 /* SessionEndedMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndedMetric.swift; sourceTree = ""; }; + 6174D6122BFDF16C00EC7469 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; + 6174D6152BFDF29B00EC7469 /* BundleTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleTypeTests.swift; sourceTree = ""; }; + 6174D6192BFE449300EC7469 /* SessionEndedMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndedMetricTests.swift; sourceTree = ""; }; + 6174D61C2C007B3300EC7469 /* ModuleName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleName.swift; sourceTree = ""; }; + 6174D61F2C009C6300EC7469 /* SessionEndedMetricController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndedMetricController.swift; sourceTree = ""; }; + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceContext.swift; sourceTree = ""; }; + 617699172A860D9D0030022B /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + 6176991A2A86121B0030022B /* HTTPClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClientMock.swift; sourceTree = ""; }; + 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datadog+MultipleInstancesIntegrationTests.swift"; sourceTree = ""; }; + 617699202A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugManualTraceInjectionViewController.swift; sourceTree = ""; }; + 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitorTests.swift; sourceTree = ""; }; + 61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugRUMSessionViewController.swift; sourceTree = ""; }; + 61776D4D273E6D9F00F93802 /* SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI.swift; sourceTree = ""; }; + 61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDebuggingTests.swift; sourceTree = ""; }; + 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingCrashReportTests.swift; sourceTree = ""; }; + 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObjcAppLaunchHandler.h; sourceTree = ""; }; + 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObjcAppLaunchHandler.m; sourceTree = ""; }; + 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitorTests.swift; sourceTree = ""; }; + 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMApplicationScopeTests.swift; sourceTree = ""; }; + 617B954124BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitorConfigurationTests.swift; sourceTree = ""; }; + 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUserActionScopeTests.swift; sourceTree = ""; }; + 618236882710560900125326 /* DebugWebviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugWebviewViewController.swift; sourceTree = ""; }; + 618353BB2A69470A0085F84A /* CoreMetricsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMetricsIntegrationTests.swift; sourceTree = ""; }; + 6184751426EFCF1300C7C9C5 /* DatadogTestsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTestsObserver.swift; sourceTree = ""; }; + 6184751726EFD03400C7C9C5 /* DatadogTestsObserverLoader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DatadogTestsObserverLoader.m; sourceTree = ""; }; + 6185EB0F25FA94A700B43E2E /* Base.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.local.xcconfig; sourceTree = ""; }; + 6185F4AD26FE1956001A7641 /* SpanE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanE2ETests.swift; sourceTree = ""; }; + 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMCommandTests.swift; sourceTree = ""; }; + 618715F824DC13A100FC0F69 /* RUMDataModelsMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataModelsMapping.swift; sourceTree = ""; }; + 618715FB24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataModelsMappingTests.swift; sourceTree = ""; }; + 6187A53826FCBE240015D94A /* TracerE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerE2ETests.swift; sourceTree = ""; }; + 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMConfigurationTests.swift; sourceTree = ""; }; + 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverMock.swift; sourceTree = ""; }; + 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanWriteContextTests.swift; sourceTree = ""; }; 618C365E248E85B400520CDE /* DateFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormattingTests.swift; sourceTree = ""; }; + 618D9DE6263AD78900A3FAD2 /* SpanEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanEventMapper.swift; sourceTree = ""; }; + 618DCFD624C7265300589570 /* RUMUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUUID.swift; sourceTree = ""; }; + 618DCFD824C7269500589570 /* RUMUUIDGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUUIDGenerator.swift; sourceTree = ""; }; + 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScopeTests.swift; sourceTree = ""; }; + 618E13A92524B8700098C6B0 /* HTTPHeadersReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersReader.swift; sourceTree = ""; }; + 618E13B02524B8F80098C6B0 /* TracingHTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingHTTPHeaders.swift; sourceTree = ""; }; + 618F9840265BC486009959F8 /* E2EInstrumentationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = E2EInstrumentationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 618F9842265BC486009959F8 /* E2EInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EInstrumentationTests.swift; sourceTree = ""; }; + 618F9844265BC486009959F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = E2EInstrumentationTests.xcconfig; sourceTree = ""; }; + 618F984D265BC905009959F8 /* E2EConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EConfig.swift; sourceTree = ""; }; + 6194B9292BB4116A00179430 /* RUMDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDataStore.swift; sourceTree = ""; }; + 6194B92C2BB43F9C00179430 /* FatalErrorContextNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifier.swift; sourceTree = ""; }; + 6194B92F2BB451C100179430 /* NonFatalAppHangsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonFatalAppHangsHandler.swift; sourceTree = ""; }; + 6194B9322BB451DB00179430 /* FatalAppHangsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalAppHangsHandler.swift; sourceTree = ""; }; + 6194D51B287ECDC00091547D /* ConsoleLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLoggerTests.swift; sourceTree = ""; }; + 6194E4B828785BFD00EB6307 /* RemoteLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteLogger.swift; sourceTree = ""; }; + 6194E4BB2878AF7600EB6307 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; + 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewScopeTests.swift; sourceTree = ""; }; + 6199362B265BA958009D7EA8 /* E2E.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = E2E.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6199362D265BA959009D7EA8 /* E2EAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EAppDelegate.swift; sourceTree = ""; }; + 61993636265BA95A009D7EA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 61993639265BA95A009D7EA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 6199363B265BA95A009D7EA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61993641265BAD2D009D7EA8 /* E2E.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = E2E.xcconfig; sourceTree = ""; }; + 61993665265BBEDC009D7EA8 /* E2ETests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = E2ETests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 61993667265BBEDC009D7EA8 /* E2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2ETests.swift; sourceTree = ""; }; + 61993669265BBEDC009D7EA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61993672265BC029009D7EA8 /* E2ETests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = E2ETests.xcconfig; sourceTree = ""; }; + 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceConfigurationTests.swift; sourceTree = ""; }; + 619CE7602A458D66005588CB /* TraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceTests.swift; sourceTree = ""; }; + 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalRUMAttributes.swift; sourceTree = ""; }; + 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogCoreProxy.swift; sourceTree = ""; }; + 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMConfigurationTests.swift; sourceTree = ""; }; + 61A2CC232A44454D0000FF25 /* DDRUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMTests.swift; sourceTree = ""; }; + 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceConfiguration.swift; sourceTree = ""; }; + 61A2CC382A44B0EA0000FF25 /* Trace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trace.swift; sourceTree = ""; }; + 61A2CC3B2A44BED30000FF25 /* Tracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracer.swift; sourceTree = ""; }; + 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRule.swift; sourceTree = ""; }; + 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRuleTests.swift; sourceTree = ""; }; + 61A763D9252DB2B3005A23F2 /* DatadogTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatadogTests-Bridging-Header.h"; sourceTree = ""; }; + 61A763DA252DB2B3005A23F2 /* NSURLSessionBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionBridge.h; sourceTree = ""; }; + 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionBridge.m; sourceTree = ""; }; 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeatureMocks.swift; sourceTree = ""; }; - 61AD4E3724531500006E34EA /* DataFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFormat.swift; sourceTree = ""; }; - 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeatureTests.swift; sourceTree = ""; }; - 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerBuilderTests.swift; sourceTree = ""; }; - 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUIDGeneratorTests.swift; sourceTree = ""; }; - 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendLogsFixtureViewController.swift; sourceTree = ""; }; - 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTracesFixtureViewController.swift; sourceTree = ""; }; - 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsHelpers.swift; sourceTree = ""; }; - 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingIntegrationTests.swift; sourceTree = ""; }; + 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTraceFeatureTests.swift; sourceTree = ""; }; + 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewMessageTests.swift; sourceTree = ""; }; + 61AE74162AD7DA9B008DB9BB /* FeatureMessageMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureMessageMocks.swift; sourceTree = ""; }; + 61B03ECC274FF00E00EB1AE1 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 61B22E5924F3E6B700DC26D2 /* RUMDebugging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDebugging.swift; sourceTree = ""; }; + 61B3BD51266128D300A9BEF0 /* LoggerE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerE2ETests.swift; sourceTree = ""; }; + 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceIDGeneratorTests.swift; sourceTree = ""; }; + 61B5E42026DF85C7000B0A5F /* DDRUMMonitor+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDRUMMonitor+apiTests.m"; sourceTree = ""; }; + 61B5E42626DFB145000B0A5F /* DDDatadog+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDDatadog+apiTests.m"; sourceTree = ""; }; + 61B5E42826DFB60A000B0A5F /* DDConfiguration+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDConfiguration+apiTests.m"; sourceTree = ""; }; + 61B5E42A26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDNSURLSessionDelegate+apiTests.m"; sourceTree = ""; }; + 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogCrashReporting.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 61B7885625C180CB002675B5 /* DatadogCrashReporting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DatadogCrashReporting.h; sourceTree = ""; }; + 61B7885725C180CB002675B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61B7885C25C180CB002675B5 /* DatadogCrashReportingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogCrashReportingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 61B7886125C180CB002675B5 /* CrashReportingPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingPluginTests.swift; sourceTree = ""; }; + 61B7886325C180CB002675B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61B8BA90281812F60068AFF4 /* KronosInternetAddressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KronosInternetAddressTests.swift; sourceTree = ""; }; + 61BAD46926415FCE001886CA /* OTSpanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTSpanTests.swift; sourceTree = ""; }; 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePreset.swift; sourceTree = ""; }; + 61BBD19624ED50040023E65F /* DatadogConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogConfigurationTests.swift; sourceTree = ""; }; + 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewIdentifierTests.swift; sourceTree = ""; }; + 61C2C20624C098FC00C0321C /* RUMSessionScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionScope.swift; sourceTree = ""; }; + 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 = ""; }; - 61C5A8732450989E00DA608C /* OpenTracing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenTracing.framework; path = ../Carthage/Build/iOS/OpenTracing.framework; 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 = ""; }; + 61C3E63A24BF1A4B008053F2 /* RUMCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMCommand.swift; sourceTree = ""; }; + 61C3E63D24BF1B91008053F2 /* RUMApplicationScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMApplicationScope.swift; sourceTree = ""; }; + 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryInterceptorTests.swift; sourceTree = ""; }; 61C5A87824509A0C00DA608C /* DDSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpan.swift; sourceTree = ""; }; 61C5A87924509A0C00DA608C /* DDNoOps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDNoOps.swift; sourceTree = ""; }; - 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracingUUIDGenerator.swift; sourceTree = ""; }; 61C5A87C24509A0C00DA608C /* Casting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; 61C5A87D24509A0C00DA608C /* Warnings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Warnings.swift; sourceTree = ""; }; 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpanContext.swift; sourceTree = ""; }; - 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanFileOutput.swift; sourceTree = ""; }; - 61C5A88124509A0C00DA608C /* SpanOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanOutput.swift; sourceTree = ""; }; 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeadersWriter.swift; sourceTree = ""; }; - 61C5A88D24509A1F00DA608C /* Tracer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tracer.swift; sourceTree = ""; }; - 61C5A88F24509AA700DA608C /* TracingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeature.swift; sourceTree = ""; }; 61C5A89524509BF600DA608C /* TracerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracerTests.swift; sourceTree = ""; }; 61C5A89824509C1100DA608C /* DDSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = ""; }; 61C5A89A24509C1100DA608C /* WarningsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarningsTests.swift; sourceTree = ""; }; - 61C5A89B24509C1100DA608C /* UUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = ""; }; - 61C5A89C24509C1100DA608C /* Casting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; - 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEncoder.swift; sourceTree = ""; }; - 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanBuilder.swift; sourceTree = ""; }; - 61D447E124917F8F00649287 /* DateFormatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatting.swift; sourceTree = ""; }; - 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUIDTests.swift; sourceTree = ""; }; - 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanBuilderTests.swift; sourceTree = ""; }; - 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanFileOutputTests.swift; sourceTree = ""; }; + 61C5A89C24509C1100DA608C /* Casting+Tracing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Casting+Tracing.swift"; sourceTree = ""; }; + 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEventEncoder.swift; sourceTree = ""; }; + 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEventBuilder.swift; sourceTree = ""; }; + 61C713A02A3B78F900FA735A /* RUMMonitorProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMMonitorProtocol.swift; sourceTree = ""; }; + 61C713A12A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RUMMonitorProtocol+Internal.swift"; sourceTree = ""; }; + 61C713A22A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RUMMonitorProtocol+Convenience.swift"; sourceTree = ""; }; + 61C713A92A3B790B00FA735A /* Monitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Monitor.swift; sourceTree = ""; }; + 61C713AC2A3B793E00FA735A /* RUMMonitorProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMMonitorProtocolTests.swift; sourceTree = ""; }; + 61C713B22A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMMonitorProtocol+InternalTests.swift"; sourceTree = ""; }; + 61C713B52A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMMonitorProtocol+ConvenienceTests.swift"; sourceTree = ""; }; + 61C713B82A3C935C00FA735A /* RUM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUM.swift; sourceTree = ""; }; + 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMInstrumentationTests.swift; sourceTree = ""; }; + 61C713BF2A3C9DAD00FA735A /* RequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilderTests.swift; sourceTree = ""; }; + 61C713C92A3DC22700FA735A /* RUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTests.swift; sourceTree = ""; }; + 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRegistrationCoreMock.swift; sourceTree = ""; }; + 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyHelpers.swift; sourceTree = ""; }; + 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monitor+GlobalAttributesTests.swift"; sourceTree = ""; }; + 61CE58592B48174D00479510 /* SpanWriteContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanWriteContext.swift; sourceTree = ""; }; + 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUMDataModels+objcTests.swift"; sourceTree = ""; }; + 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosInternetAddress.swift; sourceTree = ""; }; + 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosDNSResolver.swift; sourceTree = ""; }; + 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorage.swift; sourceTree = ""; }; + 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPPacket.swift; sourceTree = ""; }; + 61D3E0CC277B23F0008BE766 /* KronosClock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosClock.swift; sourceTree = ""; }; + 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KronosData+Bytes.swift"; sourceTree = ""; }; + 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPClient.swift; sourceTree = ""; }; + 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPProtocol.swift; sourceTree = ""; }; + 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeFreeze.swift; sourceTree = ""; }; + 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KronosNSTimer+ClosureKit.swift"; sourceTree = ""; }; + 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosNTPPacketTests.swift; sourceTree = ""; }; + 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorageTests.swift; sourceTree = ""; }; + 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KronosE2ETests.swift; sourceTree = ""; }; + 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatusTests.swift; sourceTree = ""; }; + 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorBuilder.swift; sourceTree = ""; }; + 61DA8CA828609C5B0074A606 /* Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Directories.swift; sourceTree = ""; }; + 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoriesTests.swift; sourceTree = ""; }; + 61DA8CAE28620C760074A606 /* Cryptography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cryptography.swift; sourceTree = ""; }; + 61DA8CB1286215DE0074A606 /* CryptographyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptographyTests.swift; sourceTree = ""; }; + 61DA8CB728647A500074A606 /* InternalLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalLoggerTests.swift; sourceTree = ""; }; + 61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomObjcViewController.h; sourceTree = ""; }; + 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomObjcViewController.m; sourceTree = ""; }; + 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndedMetricControllerTests.swift; sourceTree = ""; }; + 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionEndedMetricIntegrationTests.swift; sourceTree = ""; }; + 61DCC84D2C071DCD00CB59E5 /* TelemetryInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryInterceptor.swift; sourceTree = ""; }; + 61DE333525C8278A008E3EC2 /* CrashReportingPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingPlugin.swift; sourceTree = ""; }; + 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceIDTests.swift; sourceTree = ""; }; + 61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanEventBuilderTests.swift; sourceTree = ""; }; 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDataMatcher.swift; sourceTree = ""; }; 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanMatcher.swift; sourceTree = ""; }; + 61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMFeatureTests.swift; sourceTree = ""; }; + 61E5333024B75DFC003D6C4E /* RUMFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMFeatureMocks.swift; sourceTree = ""; }; + 61E5333524B84B43003D6C4E /* RUMMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitor.swift; sourceTree = ""; }; + 61E5333724B84EE2003D6C4E /* DebugRUMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugRUMViewController.swift; sourceTree = ""; }; + 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartingRUMSessionTests.swift; sourceTree = ""; }; 61E909E624A24DD3005EA2DE /* OTSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSpan.swift; sourceTree = ""; }; 61E909E724A24DD3005EA2DE /* OTFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTFormat.swift; sourceTree = ""; }; - 61E909E824A24DD3005EA2DE /* OTGlobal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTGlobal.swift; sourceTree = ""; }; 61E909E924A24DD3005EA2DE /* OTTracer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTracer.swift; sourceTree = ""; }; 61E909EA24A24DD3005EA2DE /* OTReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTReference.swift; sourceTree = ""; }; 61E909EB24A24DD3005EA2DE /* OTConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTConstants.swift; sourceTree = ""; }; 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSpanContext.swift; sourceTree = ""; }; - 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTGlobalTests.swift; sourceTree = ""; }; - 61E917CE2464270500E6C631 /* EncodableValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableValueTests.swift; sourceTree = ""; }; - 61E917D02465423600E6C631 /* TracerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfiguration.swift; sourceTree = ""; }; - 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfigurationTests.swift; sourceTree = ""; }; + 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportExporterTests.swift; sourceTree = ""; }; + 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatus.swift; sourceTree = ""; }; + 61EF78C0257F842000EDCCB3 /* FeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureTests.swift; sourceTree = ""; }; 61F1A6192498A51700075390 /* CoreMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMocks.swift; sourceTree = ""; }; 61F1A620249A45E400075390 /* DDSpanContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanContextTests.swift; sourceTree = ""; }; - 61F1A622249B811200075390 /* Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoding.swift; sourceTree = ""; }; - 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogConfigurationTests.swift; sourceTree = ""; }; + 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingFeatureMocks.swift; sourceTree = ""; }; + 61F2724825C943C500D54BF8 /* CrashReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterTests.swift; sourceTree = ""; }; + 61F2727325C9509D00D54BF8 /* ThirdPartyCrashReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyCrashReporter.swift; sourceTree = ""; }; + 61F2728A25C9561A00D54BF8 /* PLCrashReporterIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLCrashReporterIntegration.swift; sourceTree = ""; }; + 61F2729A25C95EB200D54BF8 /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; + 61F3CDA2251118FB00C816E5 /* UIViewControllerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerHandler.swift; sourceTree = ""; }; + 61F3CDA42511190E00C816E5 /* UIViewControllerSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerSwizzler.swift; sourceTree = ""; }; + 61F3CDA62512144600C816E5 /* UIKitRUMViewsPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicate.swift; sourceTree = ""; }; + 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerSwizzlerTests.swift; sourceTree = ""; }; + 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogTracer+SamplingTests.swift"; sourceTree = ""; }; + 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersReaderTests.swift; sourceTree = ""; }; + 61F3E36C2BC7D66700C7881E /* HeadBasedSamplingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadBasedSamplingTests.swift; sourceTree = ""; }; + 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugCrashReportingWithRUMViewController.swift; sourceTree = ""; }; + 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+TLV.swift"; sourceTree = ""; }; + 61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlockReader.swift; sourceTree = ""; }; + 61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlockReaderTests.swift; sourceTree = ""; }; + 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+TLVTests.swift"; sourceTree = ""; }; + 61F930CA2BA213AC005F0EE2 /* AppHang.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHang.swift; sourceTree = ""; }; + 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionMatcher.swift; sourceTree = ""; }; 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeatureMocks.swift; sourceTree = ""; }; - 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeatureTests.swift; sourceTree = ""; }; - 9E330A8B24ADE1250031408E /* DatadogTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatadogTests-Bridging-Header.h"; sourceTree = ""; }; - 9E330A8C24ADE1250031408E /* NSURLSessionBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionBridge.h; sourceTree = ""; }; - 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionBridge.m; sourceTree = ""; }; + 61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogLogsFeatureTests.swift; sourceTree = ""; }; + 61FC5F3425CC1898006BB4DE /* CrashContextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashContextProviderTests.swift; sourceTree = ""; }; + 61FD9FCB28533EDF00214BD9 /* RUMDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDeviceInfo.swift; sourceTree = ""; }; + 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDeviceInfoTests.swift; sourceTree = ""; }; + 61FDBA1226971953001D9D43 /* CrashReportMinifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportMinifier.swift; sourceTree = ""; }; + 61FDBA14269722B4001D9D43 /* CrashReportMinifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportMinifierTests.swift; sourceTree = ""; }; + 61FDBA1626974CA9001D9D43 /* DDCrashReportBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportBuilderTests.swift; sourceTree = ""; }; + 61FF281D24B8968D000B3D9B /* RUMEventBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventBuilder.swift; sourceTree = ""; }; + 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventBuilderTests.swift; sourceTree = ""; }; + 61FF282724B8A31E000B3D9B /* RUMEventMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMEventMatcher.swift; sourceTree = ""; }; + 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 = ""; }; + 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongTaskObserver.swift; sourceTree = ""; }; 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionsTests.swift; sourceTree = ""; }; - 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingAutoInstrumentationTests.swift; sourceTree = ""; }; - 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; - 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzler.swift; sourceTree = ""; }; - 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzlerTests.swift; sourceTree = ""; }; - 9E58E8E024615C75008E5063 /* JSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoder.swift; sourceTree = ""; }; + 9E53889B2773C4B300A7DC42 /* WebViewEventReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEventReceiverTests.swift; sourceTree = ""; }; + 9E55407B25812D1C00F6E3AD /* RUM+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RUM+objc.swift"; sourceTree = ""; }; 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoderTests.swift; sourceTree = ""; }; + 9E5B6D2D270C84B4002499B8 /* RUMMonitorE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitorE2ETests.swift; sourceTree = ""; }; + 9E5B6D2F270C85AB002499B8 /* RUMUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMUtils.swift; sourceTree = ""; }; + 9E5B6D31270DE9E5002499B8 /* RUMTrackingConsentE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMTrackingConsentE2ETests.swift; sourceTree = ""; }; + 9E5BD8052819742C00CB568E /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; + 9E64849C27031071007CCD7B /* RUMGlobalE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMGlobalE2ETests.swift; sourceTree = ""; }; 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcExceptionHandler.m; sourceTree = ""; }; 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcExceptionHandler.h; sourceTree = ""; }; - 9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = ""; }; - 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; - 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingAutoInstrumentation.swift; sourceTree = ""; }; - 9EF49F1624476FBD004F2CA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogIntegrationTests.xcconfig; sourceTree = ""; }; + 9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReaderTests.swift; sourceTree = ""; }; + 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoSampler.swift; sourceTree = ""; }; + 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReader.swift; sourceTree = ""; }; + 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReader.swift; sourceTree = ""; }; + 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReaderTests.swift; sourceTree = ""; }; + 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNSURLSessionDelegateTests.swift; sourceTree = ""; }; + A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskCoordinator.swift; sourceTree = ""; }; + A70ADCD12B583B1300321BC9 /* UIImageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageResource.swift; sourceTree = ""; }; + A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriterTests.swift; sourceTree = ""; }; + A71265852B17980C007D63CE /* MockFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeature.swift; sourceTree = ""; }; + A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeaders.swift; sourceTree = ""; }; + A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersWriter.swift; sourceTree = ""; }; + A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersReader.swift; sourceTree = ""; }; + A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersWriterTests.swift; sourceTree = ""; }; + A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = W3CHTTPHeadersReaderTests.swift; sourceTree = ""; }; + A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "W3CHTTPHeadersWriter+objc.swift"; sourceTree = ""; }; + A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDW3CHTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; + A73A54972B16406900E1F7E3 /* ResourcesFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesFeature.swift; sourceTree = ""; }; + A74A72802B0CEE4900771FEB /* ResourceRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceRequestBuilder.swift; sourceTree = ""; }; + A74A72842B10CC6700771FEB /* ResourceRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceRequestBuilderTests.swift; sourceTree = ""; }; + A74A72862B10CE4100771FEB /* ResourceMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceMocks.swift; sourceTree = ""; }; + A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartBuilderSpy.swift; sourceTree = ""; }; + A795069B2B974C8100AC4814 /* SessionReplay+objc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionReplay+objc.swift"; sourceTree = ""; }; + A795069D2B974CAA00AC4814 /* DDSessionReplay+apiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "DDSessionReplay+apiTests.m"; sourceTree = ""; }; + A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersWriterTests.swift; sourceTree = ""; }; + A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "B3HTTPHeadersWriter+objc.swift"; sourceTree = ""; }; + A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersReaderTests.swift; sourceTree = ""; }; + A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDB3HTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; + A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesProcessor.swift; sourceTree = ""; }; + A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedRecord.swift; sourceTree = ""; }; + A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRDataModels.swift; sourceTree = ""; }; + A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrichedResource.swift; sourceTree = ""; }; + A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SRDataModels+UIKit.swift"; sourceTree = ""; }; + A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; + A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionBackgroundTaskCoordinatorTests.swift; sourceTree = ""; }; + A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorSpy.swift; sourceTree = ""; }; + A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceProcessorTests.swift; sourceTree = ""; }; + A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; + A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDUIKitRUMActionsPredicateTests.swift; sourceTree = ""; }; + A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesWriter.swift; sourceTree = ""; }; + A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageResourceTests.swift; sourceTree = ""; }; + A7F773D32924EA2D00AC1A62 /* B3HTTPHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = B3HTTPHeaders.swift; sourceTree = ""; }; + A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersWriter.swift; sourceTree = ""; }; + A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersReader.swift; sourceTree = ""; }; + 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 = ""; }; + D20605A2287464F40047275C /* ContextValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValuePublisher.swift; sourceTree = ""; }; + D20605A5287476230047275C /* ServerOffsetPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisher.swift; sourceTree = ""; }; + D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoPublisher.swift; sourceTree = ""; }; + D20605B12874E1660047275C /* CarrierInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrierInfoPublisher.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageReceiver.swift; sourceTree = ""; }; + D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstPartyHosts.swift; sourceTree = ""; }; + D2160C9629C0DE5600FAA9A5 /* TracingHeaderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracingHeaderType.swift; sourceTree = ""; }; + D2160C9729C0DE5700FAA9A5 /* HostsSanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizer.swift; sourceTree = ""; }; + D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationFeature.swift; sourceTree = ""; }; + D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskInterception.swift; sourceTree = ""; }; + D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogURLSessionDelegate.swift; sourceTree = ""; }; + D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationFeatureTests.swift; sourceTree = ""; }; + D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstPartyHostsTests.swift; sourceTree = ""; }; + D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsSanitizerTests.swift; sourceTree = ""; }; + D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskInterceptionTests.swift; sourceTree = ""; }; + D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDelegateAsSuperclassTests.swift; sourceTree = ""; }; + D2160CE329C0DFED00FAA9A5 /* MethodSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MethodSwizzler.swift; sourceTree = ""; }; + D2160CE829C0E00200FAA9A5 /* MethodSwizzlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MethodSwizzlerTests.swift; sourceTree = ""; }; + D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogURLSessionHandler.swift; sourceTree = ""; }; + D2160CEF29C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFeatureCoreMock.swift; sourceTree = ""; }; + D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadMocks.swift; sourceTree = ""; }; + 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 = ""; }; + D21C26D028A64599005DD405 /* MessageBusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBusTests.swift; sourceTree = ""; }; + D21C26D628A647DB005DD405 /* FeatureMessageReceiverMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureMessageReceiverMock.swift; sourceTree = ""; }; + D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogMessageReceiverTests.swift; sourceTree = ""; }; + 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 = ""; }; + D22C5BCD2A98A65D0024CC1F /* Baggages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baggages.swift; sourceTree = ""; }; + D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+Convenience.swift"; sourceTree = ""; }; + D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Convenience.swift"; sourceTree = ""; }; + D23039A5298D513C001A1FA3 /* DatadogInternal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogInternal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D23039AD298D5234001A1FA3 /* DD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DD.swift; sourceTree = ""; }; + D23039AF298D5235001A1FA3 /* Writer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Writer.swift; sourceTree = ""; }; + D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogCoreProtocol.swift; sourceTree = ""; }; + D23039B3298D5235001A1FA3 /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + D23039B4298D5235001A1FA3 /* UserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; + D23039B5298D5235001A1FA3 /* BatteryStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryStatus.swift; sourceTree = ""; }; + D23039B6298D5235001A1FA3 /* CarrierInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarrierInfo.swift; sourceTree = ""; }; + D23039B7298D5235001A1FA3 /* DateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; + D23039B8298D5235001A1FA3 /* Sysctl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sysctl.swift; sourceTree = ""; }; + D23039B9298D5235001A1FA3 /* NetworkConnectionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfo.swift; sourceTree = ""; }; + D23039BA298D5235001A1FA3 /* DatadogContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogContext.swift; sourceTree = ""; }; + D23039BB298D5235001A1FA3 /* TrackingConsent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingConsent.swift; sourceTree = ""; }; + D23039BC298D5235001A1FA3 /* DeviceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; + D23039BD298D5235001A1FA3 /* DatadogFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogFeature.swift; sourceTree = ""; }; + D23039BE298D5235001A1FA3 /* LaunchTime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchTime.swift; sourceTree = ""; }; + D23039C1298D5235001A1FA3 /* FeatureMessageReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureMessageReceiver.swift; sourceTree = ""; }; + D23039C2298D5235001A1FA3 /* FeatureMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureMessage.swift; sourceTree = ""; }; + D23039C4298D5235001A1FA3 /* AnyEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncoder.swift; sourceTree = ""; }; + D23039C5298D5235001A1FA3 /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; + D23039C6298D5235001A1FA3 /* AnyDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecoder.swift; sourceTree = ""; }; + D23039C7298D5235001A1FA3 /* DynamicCodingKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicCodingKey.swift; sourceTree = ""; }; + D23039C8298D5235001A1FA3 /* AnyCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = ""; }; + D23039C9298D5235001A1FA3 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; + D23039CB298D5235001A1FA3 /* Attributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Attributes.swift; sourceTree = ""; }; + D23039CC298D5235001A1FA3 /* AttributesSanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesSanitizer.swift; sourceTree = ""; }; + D23039CE298D5235001A1FA3 /* InternalLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalLogger.swift; sourceTree = ""; }; + D23039CF298D5235001A1FA3 /* CoreLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreLogger.swift; sourceTree = ""; }; + D23039D0298D5235001A1FA3 /* Telemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; + D23039D2298D5235001A1FA3 /* URLRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestBuilder.swift; sourceTree = ""; }; + D23039D3298D5235001A1FA3 /* DataFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataFormat.swift; sourceTree = ""; }; + D23039D4298D5235001A1FA3 /* DataCompression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataCompression.swift; sourceTree = ""; }; + D23039D5298D5235001A1FA3 /* FeatureRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureRequestBuilder.swift; sourceTree = ""; }; + D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Foundation+Datadog.swift"; sourceTree = ""; }; + D23039D8298D5235001A1FA3 /* DatadogExtended.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogExtended.swift; sourceTree = ""; }; + D23039D9298D5235001A1FA3 /* DateFormatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatting.swift; sourceTree = ""; }; + D23039DB298D5235001A1FA3 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; + D23039DC298D5235001A1FA3 /* DDError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDError.swift; sourceTree = ""; }; + D2303A09298D5412001A1FA3 /* AsyncWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncWriter.swift; sourceTree = ""; }; + D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalExtended.swift; sourceTree = ""; }; + D234613028B7712F00055D4C /* FeatureContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureContextTests.swift; sourceTree = ""; }; + D236BE2729520FED00676E67 /* CrashReportReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportReceiver.swift; sourceTree = ""; }; + D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogRUM.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D23F8ECD29DDCD38001CFAE8 /* DatadogRUMTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogRUMTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D240684D27CE6C9E00C04F44 /* Example tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D240688527CFA64A00C04F44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + D242C2A02A14D747004B4980 /* RemoteLoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteLoggerTests.swift; sourceTree = ""; }; + D2432CF829EDB22C00D93657 /* Flushable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Flushable.swift; sourceTree = ""; }; + D243BBBF276C9D640019C857 /* PLCrashReporterIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PLCrashReporterIntegrationTests.swift; sourceTree = ""; }; + D243BBEB29A614CE000B9CEC /* LoggerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerProtocol.swift; sourceTree = ""; }; + D243BBF129A6209C000B9CEC /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; + D243BBF429A620CC000B9CEC /* MessageReceivers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceivers.swift; sourceTree = ""; }; + D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensionsTests.swift; sourceTree = ""; }; + D248ED4728081B9B00B315B4 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; + D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIViewModifier.swift; sourceTree = ""; }; + D24985A12728048B00B4F72D /* SwiftUIViewHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIViewHandler.swift; sourceTree = ""; }; + D24C9C3E29A79772002057CF /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + D24C9C4C29A7B9CA002057CF /* LogsMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsMocks.swift; sourceTree = ""; }; + D24C9C5129A7BD12002057CF /* SamplerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplerMock.swift; sourceTree = ""; }; + D24C9C5429A7C5F3002057CF /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; + D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDErrorMocks.swift; sourceTree = ""; }; + D24C9C7029A7D57A002057CF /* DirectoriesMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoriesMock.swift; sourceTree = ""; }; + D250850F2976E30000E931C3 /* DatadogRemoteFeatureMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogRemoteFeatureMock.swift; sourceTree = ""; }; + D253EE952B988CA90010B589 /* ViewCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCache.swift; sourceTree = ""; }; + D253EE982B98B3690010B589 /* ViewCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCacheTests.swift; sourceTree = ""; }; + D2546BF029AF4F550054E00B /* DatadogTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTracer.swift; sourceTree = ""; }; + D2546C0329AF55AA0054E00B /* TraceFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceFeature.swift; sourceTree = ""; }; + D2546C0729AF55E90054E00B /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; + D2546C0A29AF56270054E00B /* MessageReceivers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceivers.swift; sourceTree = ""; }; + D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEventIntegrationTests.swift; sourceTree = ""; }; + D2553806288AA84F00727FAD /* UploadMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadMock.swift; sourceTree = ""; }; + D2553825288F0B1A00727FAD /* BatteryStatusPublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryStatusPublisher.swift; sourceTree = ""; }; + D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LowPowerModePublisher.swift; sourceTree = ""; }; + D257953E298ABA65008A1BE5 /* TestUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TestUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2579547298ABB04008A1BE5 /* FileWriterMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileWriterMock.swift; sourceTree = ""; }; + D2579548298ABB04008A1BE5 /* DatadogContextMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogContextMock.swift; sourceTree = ""; }; + D2579549298ABB04008A1BE5 /* FeatureBaggageMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureBaggageMock.swift; sourceTree = ""; }; + D257954A298ABB04008A1BE5 /* PassthroughCoreMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassthroughCoreMock.swift; sourceTree = ""; }; + D257954B298ABB04008A1BE5 /* FoundationMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationMocks.swift; sourceTree = ""; }; + D257954C298ABB04008A1BE5 /* AttributesMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesMocks.swift; sourceTree = ""; }; + D257954E298ABB04008A1BE5 /* Encoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Encoding.swift; sourceTree = ""; }; + D257954F298ABB04008A1BE5 /* DDAssert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDAssert.swift; sourceTree = ""; }; + D2579550298ABB04008A1BE5 /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; + D2579551298ABB04008A1BE5 /* XCTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; + D257958B298ABB83008A1BE5 /* TestUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TestUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2579591298ABCED008A1BE5 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + D2579593298ABCF5008A1BE5 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/AppleTVOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + D25BADA029C1EF3000112069 /* TracingURLSessionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandler.swift; sourceTree = ""; }; + D25C834B2B8657CF008E73B1 /* SegmentJSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentJSONTests.swift; sourceTree = ""; }; + D25CFA9E29C85FA400E3A43D /* TracingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeatureMocks.swift; sourceTree = ""; }; + D25CFAA129C8644E00E3A43D /* Casting+Tracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Casting+Tracing.swift"; sourceTree = ""; }; + D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogTrace.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D25EE93B29C4C3C300CE3839 /* DatadogTraceTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogTraceTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D25FF2E729CC6B680063802D /* RUMFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMFeature.swift; sourceTree = ""; }; + D25FF2EA29CC6D6F0063802D /* RUMConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMConfiguration.swift; sourceTree = ""; }; + D25FF2ED29CC73240063802D /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; + D25FF2F329CC88060063802D /* RUMBaggageKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMBaggageKeys.swift; sourceTree = ""; }; + D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformancePresetOverride.swift; sourceTree = ""; }; + D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+ConvenienceTests.swift"; sourceTree = ""; }; + D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ConvenienceTests.swift"; sourceTree = ""; }; + D26416B52A30E84F00BCD9F7 /* CoreRegistryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreRegistryTest.swift; sourceTree = ""; }; + D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisherTests.swift; sourceTree = ""; }; + D26C49B52889416300802B2D /* UploadPerformancePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPerformancePreset.swift; sourceTree = ""; }; + 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 = ""; }; + D29294DF291D5ECD00F8EFF9 /* ApplicationVersionPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationVersionPublisher.swift; sourceTree = ""; }; + D29294E2291D652900F8EFF9 /* ApplicationVersionPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationVersionPublisherTests.swift; sourceTree = ""; }; + D293302B2A137DAD0029C9EA /* CrashReportingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportingFeature.swift; sourceTree = ""; }; + D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionInterceptor.swift; sourceTree = ""; }; + D29732462A5C108700827599 /* DDScriptMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDScriptMessageHandler.swift; sourceTree = ""; }; + D29732472A5C108700827599 /* MessageEmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEmitter.swift; sourceTree = ""; }; + D297324F2A5C109A00827599 /* MessageEmitterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEmitterTests.swift; sourceTree = ""; }; + D29732502A5C109A00827599 /* WebViewTrackingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewTrackingTests.swift; sourceTree = ""; }; + D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewsHandlerTests.swift; sourceTree = ""; }; + D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogRUM.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D29A9F3B29DD84AB005C54A4 /* DatadogRUMTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogRUMTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExtensions.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrierInfoPublisherTests.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogCore.swift; sourceTree = ""; }; + D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDHTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; + D2BCB11E29D30AF000737A9A /* URLSessionRUMResourcesHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionRUMResourcesHandler.swift; sourceTree = ""; }; + D2BCB12129D34A5F00737A9A /* URLSessionRUMResourcesHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionRUMResourcesHandlerTests.swift; sourceTree = ""; }; + D2BCB2A02B7B8107005C2AAB /* WKWebViewRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKWebViewRecorder.swift; sourceTree = ""; }; + D2BCB2A22B7B9683005C2AAB /* WKWebViewRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKWebViewRecorderTests.swift; sourceTree = ""; }; + D2BEEDAB2B3356710065F3AC /* URLSessionTaskSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzler.swift; sourceTree = ""; }; + D2BEEDAE2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskSwizzlerTests.swift; sourceTree = ""; }; + D2BEEDB12B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzler.swift; sourceTree = ""; }; + D2BEEDB42B33607D0065F3AC /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; + D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTaskDelegateSwizzlerTests.swift; sourceTree = ""; }; + D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogTrace.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2C1A57329C4F2E800946C31 /* DatadogTraceTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogTraceTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewRecordReceiver.swift; sourceTree = ""; }; + D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewRecordReceiverTests.swift; sourceTree = ""; }; + D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRecordIntegrationTests.swift; sourceTree = ""; }; + D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStatusPublisherTests.swift; sourceTree = ""; }; + D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimePublisher.swift; sourceTree = ""; }; + D2C9A2852C0F4660007526F5 /* SessionReplayConfigurationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayConfigurationMocks.swift; sourceTree = ""; }; + D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2CB6F8F27C520D400A62B57 /* DatadogCoreTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogCoreTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogObjc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogCrashReporting.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2CB6FEC27C5352300A62B57 /* DatadogCrashReportingTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogCrashReportingTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2CBC26A294383F200134409 /* WebViewEventReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewEventReceiver.swift; sourceTree = ""; }; + D2CBC26D294395A300134409 /* RUMContextAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMContextAttributes.swift; sourceTree = ""; }; + D2D30E5A2A40BF540020C553 /* Logs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logs.swift; sourceTree = ""; }; + D2D30E5D2A40CD2C0020C553 /* LogsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsTests.swift; sourceTree = ""; }; + D2D3199929E98D970004F169 /* DefaultJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultJSONEncoder.swift; sourceTree = ""; }; + D2D36DCA2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogCoreProtocolTests.swift; sourceTree = ""; }; + D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogInternal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D2DA238A298D588800C6C7E6 /* DatadogInternalTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogInternalTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2DA2395298D58F300C6C7E6 /* ReadWriteLockTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLockTests.swift; sourceTree = ""; }; + D2DA2398298D58F300C6C7E6 /* AnyEncodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodableTests.swift; sourceTree = ""; }; + D2DA2399298D58F300C6C7E6 /* AnyCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCodableTests.swift; sourceTree = ""; }; + D2DA239A298D58F300C6C7E6 /* AnyDecodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodableTests.swift; sourceTree = ""; }; + D2DA239B298D58F300C6C7E6 /* AnyCoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCoderTests.swift; sourceTree = ""; }; + D2DA239D298D58F300C6C7E6 /* AppStateHistoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStateHistoryTests.swift; sourceTree = ""; }; + D2DA239E298D58F300C6C7E6 /* DeviceInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceInfoTests.swift; sourceTree = ""; }; + D2DA23A0298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureMessageReceiverTests.swift; sourceTree = ""; }; + D2DA23C3298D59DC00C6C7E6 /* DatadogInternalTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogInternalTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2DA23C6298D5AC000C6C7E6 /* TelemetryMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryMocks.swift; sourceTree = ""; }; + D2DA23C9298D5C1300C6C7E6 /* UIKitMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; + 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 = ""; }; + D2EBEDD229B8A58E00B15732 /* TracePropagationHeadersReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracePropagationHeadersReader.swift; sourceTree = ""; }; + D2EBEDD629B8F08E00B15732 /* DDFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDFormat.swift; sourceTree = ""; }; + D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInstrumentationMocks.swift; sourceTree = ""; }; + D2EFA867286DA85700F1FAA6 /* DatadogContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogContextProvider.swift; sourceTree = ""; }; + D2EFA874286E011900F1FAA6 /* DatadogContextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogContextProviderTests.swift; sourceTree = ""; }; + D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewsHandler.swift; sourceTree = ""; }; + D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopTracerTests.swift; sourceTree = ""; }; + D2F44FBB299AA36D0074B0D9 /* Decompression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decompression.swift; sourceTree = ""; }; + D2F44FC1299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+KeyboardControlling.swift"; sourceTree = ""; }; + D2F8235229915E12003C7E99 /* DatadogSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogSite.swift; sourceTree = ""; }; + D2FB1253292E0E92005B13F8 /* TrackingConsentPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingConsentPublisher.swift; sourceTree = ""; }; + D2FB1256292E0F0B005B13F8 /* TrackingConsentPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingConsentPublisherTests.swift; sourceTree = ""; }; + D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datadog+Internal.swift"; sourceTree = ""; }; + D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = ""; }; + E11625D727B681D200E428C6 /* CITestIntegration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CITestIntegration.swift; sourceTree = ""; }; + E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CITestIntegrationTests.swift; sourceTree = ""; }; + E179FB4D28F80A6400CC2698 /* PerformanceMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetric.swift; sourceTree = ""; }; + E1B082CB25641DF9002DB9D2 /* Example.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Example.xcconfig; sourceTree = ""; }; + E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryMocks.swift; sourceTree = ""; }; + 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 */ + 3CE119FB29F7BE0000202522 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C9C6BB429F7C0C000581C43 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3CE11A0229F7BE0300202522 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C85D42A29F7C70300AFF894 /* TestUtilities.framework in Frameworks */, + 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 61133B88242393DE00786299 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61133B8C242393DE00786299 /* Datadog.framework in Frameworks */, + D20FD9D12ACC02F9004D3569 /* DatadogWebViewTracking.framework in Frameworks */, + 6175922B2A6FA8EE0073F431 /* DatadogSessionReplay.framework in Frameworks */, + 614798A32A45A4980095CB02 /* DatadogTrace.framework in Frameworks */, + 61A2CC302A4449CB0000FF25 /* DatadogRUM.framework in Frameworks */, + D2D3199729E982A30004F169 /* DatadogCrashReporting.framework in Frameworks */, + D24C9C4A29A7B35C002057CF /* DatadogLogs.framework in Frameworks */, + D2579595298AC912008A1BE5 /* TestUtilities.framework in Frameworks */, + 61133B8C242393DE00786299 /* DatadogCore.framework in Frameworks */, 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -468,1388 +3196,8235 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61133C702423993200786299 /* Datadog.framework in Frameworks */, + 6147989E2A45A42C0095CB02 /* DatadogTrace.framework in Frameworks */, + 61A2CC262A4449210000FF25 /* DatadogRUM.framework in Frameworks */, + D206BB852A41CA6800F43BA2 /* DatadogLogs.framework in Frameworks */, + 61133C702423993200786299 /* DatadogCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 61441BFF24616DE9003D8BB8 /* Frameworks */ = { + 6133D1EE2A6ED9E100384BEF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6133D1EF2A6ED9E100384BEF /* DatadogInternal.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 61441C2724616F1D003D8BB8 /* Frameworks */ = { + 6133D2002A6EDB7700384BEF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61441C44246174CE003D8BB8 /* HTTPServerMock in Frameworks */, + 6133D2012A6EDB7700384BEF /* TestUtilities.framework in Frameworks */, + 6133D20B2A6EDBC100384BEF /* DatadogSessionReplay.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 61441C6524619FE4003D8BB8 /* Frameworks */ = { + 61441BFF24616DE9003D8BB8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61441C6D24619FE4003D8BB8 /* Datadog.framework in Frameworks */, - 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */, + 6175922D2A6FADDD0073F431 /* DatadogSessionReplay.framework in Frameworks */, + 61A2CC332A44A5F60000FF25 /* DatadogRUM.framework in Frameworks */, + 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */, + D25CFA9C29C4FC6900E3A43D /* DatadogTrace.framework in Frameworks */, + 9E5BD8042819742200CB568E /* SwiftUI.framework in Frameworks */, + D240687827CF982B00C04F44 /* CrashReporter.xcframework in Frameworks */, + D240687B27CF982C00C04F44 /* DatadogCore.framework in Frameworks */, + D240687D27CF982D00C04F44 /* DatadogCrashReporting.framework in Frameworks */, + 1434A4612B7F73110072E3BB /* OpenTelemetryApi.xcframework in Frameworks */, + D24C9C4229A7A50D002057CF /* DatadogLogs.framework in Frameworks */, + D240687F27CF982F00C04F44 /* DatadogObjc.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 61133B78242393DE00786299 = { - isa = PBXGroup; - children = ( - 61133B9C2423979B00786299 /* Datadog */, - 61133C082423983800786299 /* DatadogObjc */, - 9E68FB52244707FD0013A8AA /* _Datadog_Private */, - 61133C122423990D00786299 /* DatadogTests */, - 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */, - 61441C3524617013003D8BB8 /* DatadogIntegrationTests */, - 61133C07242397F200786299 /* TargetSupport */, - 61441C0324616DE9003D8BB8 /* Example */, - 61133B83242393DE00786299 /* Products */, - 61133C6F2423993200786299 /* Frameworks */, + 61569793256CF6C300C6AADA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2303A04298D5317001A1FA3 /* DatadogInternal.framework in Frameworks */, ); - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - 61133B83242393DE00786299 /* Products */ = { - isa = PBXGroup; - children = ( - 61133B82242393DE00786299 /* Datadog.framework */, - 61133B8B242393DE00786299 /* DatadogTests.xctest */, - 61133BF0242397DA00786299 /* DatadogObjc.framework */, - 61441C0224616DE9003D8BB8 /* Example.app */, - 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */, - 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */, + 618F983D265BC486009959F8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2303998298D50F1001A1FA3 /* XCTest.framework in Frameworks */, + D2303999298D50F1001A1FA3 /* DatadogCore.framework in Frameworks */, ); - name = Products; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - 61133B84242393DE00786299 /* Datadog */ = { - isa = PBXGroup; - children = ( - 61133B85242393DE00786299 /* Datadog.h */, - 61133B86242393DE00786299 /* Info.plist */, + 61993628265BA958009D7EA8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ); - path = Datadog; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - 61133B8F242393DE00786299 /* DatadogTests */ = { - isa = PBXGroup; - children = ( - 61133B92242393DE00786299 /* Info.plist */, - 9E330A8B24ADE1250031408E /* DatadogTests-Bridging-Header.h */, + 61993662265BBEDC009D7EA8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D27D81C82A5D41F400281CC2 /* TestUtilities.framework in Frameworks */, + D2303996298D50F1001A1FA3 /* XCTest.framework in Frameworks */, + D27D81C32A5D415200281CC2 /* DatadogInternal.framework in Frameworks */, + D2303997298D50F1001A1FA3 /* DatadogCore.framework in Frameworks */, + D27D81C42A5D415200281CC2 /* DatadogLogs.framework in Frameworks */, + D27D81C52A5D415200281CC2 /* DatadogRUM.framework in Frameworks */, + D27D81C62A5D415200281CC2 /* DatadogTrace.framework in Frameworks */, + D27D81C22A5D415200281CC2 /* DatadogCrashReporting.framework in Frameworks */, + D27D81C12A5D415200281CC2 /* CrashReporter.xcframework in Frameworks */, + D27D81C72A5D415200281CC2 /* DatadogWebViewTracking.framework in Frameworks */, ); - path = DatadogTests; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885125C180CB002675B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D214DA9929DF2F6A004D0AE8 /* DatadogInternal.framework in Frameworks */, + 614ED36C260352DC00C8C519 /* CrashReporter.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885925C180CB002675B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2579599298AD95F008A1BE5 /* TestUtilities.framework in Frameworks */, + 61B7885D25C180CB002675B5 /* DatadogCrashReporting.framework in Frameworks */, + 619A29F326E64910007D62A3 /* CrashReporter.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207317929A5226A00ECBF94 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D207319729A5232A00ECBF94 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207318029A5226A00ECBF94 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7840529A5370A003B03BB /* TestUtilities.framework in Frameworks */, + D207318429A5226B00ECBF94 /* DatadogLogs.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D20731AD29A5279D00ECBF94 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C74305C29FBC0480053B80F /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23039A2298D513C001A1FA3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8E9029DDCD28001CFAE8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D23F8ECE29DDCD53001CFAE8 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8EC529DDCD38001CFAE8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D23F8EC629DDCD38001CFAE8 /* TestUtilities.framework in Frameworks */, + D23F8EC729DDCD38001CFAE8 /* DatadogRUM.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D240682F27CE6C9E00C04F44 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 61A2CC342A44A6030000FF25 /* DatadogRUM.framework in Frameworks */, + 1434A4632B7F73170072E3BB /* OpenTelemetryApi.xcframework in Frameworks */, + D25CFA9D29C4FC6E00E3A43D /* DatadogTrace.framework in Frameworks */, + 9E5BD8062819742C00CB568E /* SwiftUI.framework in Frameworks */, + D240687027CF971C00C04F44 /* CrashReporter.xcframework in Frameworks */, + D240687127CF971C00C04F44 /* DatadogCore.framework in Frameworks */, + D240687227CF971C00C04F44 /* DatadogCrashReporting.framework in Frameworks */, + D24C9C4629A7A520002057CF /* DatadogLogs.framework in Frameworks */, + D240687327CF971C00C04F44 /* DatadogObjc.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D257953B298ABA65008A1BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D26F741129ACBDA100D25622 /* DatadogInternal.framework in Frameworks */, + D2579592298ABCED008A1BE5 /* XCTest.framework in Frameworks */, + 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2579582298ABB83008A1BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D26F741229ACBDAD00D25622 /* DatadogInternal.framework in Frameworks */, + D230399E298D50F1001A1FA3 /* XCTest.framework in Frameworks */, + 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93129C4C3C300CE3839 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C5D691F2B76825500C4E07E /* OpenTelemetryApi.xcframework in Frameworks */, + D2C1A50E29C4C4EF00946C31 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93829C4C3C300CE3839 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D25EE93C29C4C3C300CE3839 /* DatadogTrace.framework in Frameworks */, + D2C1A52829C4C8CB00946C31 /* TestUtilities.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3129DD84AA005C54A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D29A9F4B29DD8525005C54A4 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3829DD84AB005C54A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D29A9FC129DDB58C005C54A4 /* TestUtilities.framework in Frameworks */, + D29A9F3C29DD84AB005C54A4 /* DatadogRUM.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2A783FA29A534F9003B03BB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7840629A53710003B03BB /* TestUtilities.framework in Frameworks */, + D2A783FB29A534F9003B03BB /* DatadogLogs.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A55329C4F2DF00946C31 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C5D69222B76826000C4E07E /* OpenTelemetryApi.xcframework in Frameworks */, + D2C1A57429C4F30000946C31 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A56B29C4F2E800946C31 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D25CFA9829C4F41900E3A43D /* DatadogTrace.framework in Frameworks */, + D25CFA9929C4F41900E3A43D /* TestUtilities.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6ECC27C50EAE00A62B57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CE604229911EDE00DB6656 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6F8627C520D400A62B57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 614798A22A45A48F0095CB02 /* DatadogTrace.framework in Frameworks */, + 61A2CC312A4449D70000FF25 /* DatadogRUM.framework in Frameworks */, + D2D3199829E982AC0004F169 /* DatadogCrashReporting.framework in Frameworks */, + D24C9C4B29A7B365002057CF /* DatadogLogs.framework in Frameworks */, + D2579596298AC927008A1BE5 /* TestUtilities.framework in Frameworks */, + D2CB6FB827C523DA00A62B57 /* DatadogCore.framework in Frameworks */, + D2CB6FB927C523DA00A62B57 /* DatadogObjc.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FA927C5217A00A62B57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 614798A02A45A46B0095CB02 /* DatadogTrace.framework in Frameworks */, + 61A2CC2B2A4449300000FF25 /* DatadogRUM.framework in Frameworks */, + D206BB8A2A41CA7000F43BA2 /* DatadogLogs.framework in Frameworks */, + D2CB6FB327C5234300A62B57 /* DatadogCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; }; - 61133B9C2423979B00786299 /* Datadog */ = { + D2CB6FCA27C5348200A62B57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D214DA9429DF2F5E004D0AE8 /* DatadogInternal.framework in Frameworks */, + D2CB6FCB27C5348200A62B57 /* CrashReporter.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FE327C5352300A62B57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D257959A298AD967008A1BE5 /* TestUtilities.framework in Frameworks */, + D2CB6FF327C5369600A62B57 /* DatadogCrashReporting.framework in Frameworks */, + D2CB6FE527C5352300A62B57 /* CrashReporter.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA237F298D57AA00C6C7E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2387298D588800C6C7E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2DA23AB298D595100C6C7E6 /* TestUtilities.framework in Frameworks */, + D2DA238E298D588A00C6C7E6 /* DatadogInternal.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA23BB298D59DC00C6C7E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D2DA23D1298D61FB00C6C7E6 /* DatadogInternal.framework in Frameworks */, + D2DA23C5298D59F300C6C7E6 /* TestUtilities.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */ = { isa = PBXGroup; children = ( - 9E9EB37624468CE90002C80B /* Datadog.modulemap */, - 61133BBB2423979B00786299 /* Datadog.swift */, - 61133BB62423979B00786299 /* Logger.swift */, - 61C5A88D24509A1F00DA608C /* Tracer.swift */, - 61E917D02465423600E6C631 /* TracerConfiguration.swift */, - 61133BB52423979B00786299 /* DatadogConfiguration.swift */, - 61133B9E2423979B00786299 /* Core */, - 61133BBC2423979B00786299 /* Logging */, - 61C5A87724509A0C00DA608C /* Tracing */, - 61216277247D1F2100AC5D67 /* FeaturesIntegration */, - 61133BB72423979B00786299 /* Utils */, - 61E909E524A24DD3005EA2DE /* OpenTracing */, + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */, + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */, + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */, ); - name = Datadog; - path = ../Sources/Datadog; + path = MemoryWarnings; sourceTree = ""; }; - 61133B9E2423979B00786299 /* Core */ = { + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */ = { isa = PBXGroup; children = ( - 614E9EB2244719FA007EE3E1 /* BundleType.swift */, - 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */, - 9E5D2D4A249137E900763FE4 /* AutoInstrumentation */, - 61133BBF2423979B00786299 /* Attributes */, - 61133B9F2423979B00786299 /* Utils */, - 61133BA12423979B00786299 /* System */, - 61133BA62423979B00786299 /* Persistence */, - 61133BAE2423979B00786299 /* Upload */, + 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */, + 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */, ); - path = Core; + path = MemoryWarnings; sourceTree = ""; }; - 61133B9F2423979B00786299 /* Utils */ = { + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */ = { isa = PBXGroup; children = ( - 61D447E124917F8F00649287 /* DateFormatting.swift */, - 61133BA02423979B00786299 /* EncodableValue.swift */, - 9E58E8E024615C75008E5063 /* JSONEncoder.swift */, + 3CFF4F8A2C09E61A006F191D /* WatchdogTerminationAppState.swift */, + 3CFF4F902C09E630006F191D /* WatchdogTerminationAppStateManager.swift */, + 3CFF4F932C09E63C006F191D /* WatchdogTerminationChecker.swift */, + 3CFF4F962C09E64C006F191D /* WatchdogTerminationMonitor.swift */, + 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */, ); - path = Utils; + path = WatchdogTerminations; sourceTree = ""; }; - 61133BA12423979B00786299 /* System */ = { + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( - 61133BA82423979B00786299 /* DateProvider.swift */, - 61133BA22423979B00786299 /* CarrierInfoProvider.swift */, - 61133BA32423979B00786299 /* MobileDevice.swift */, - 61133BA42423979B00786299 /* NetworkConnectionInfoProvider.swift */, - 61133BA52423979B00786299 /* BatteryStatusProvider.swift */, - ); - path = System; + 3CFF5D482B555F4F00FC483A /* OTelTracerProvider.swift */, + 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */, + 3C32359C2B55386C000B4258 /* OTelSpanLink.swift */, + 3CC6AD172B4F07DC00015B18 /* OTelAttributeValue+Datadog.swift */, + 3CB012DB2B482E0400557951 /* NOPOTelSpan.swift */, + 3CB012DC2B482E0400557951 /* NOPOTelSpanBuilder.swift */, + 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */, + 3C6C7FE12B459AAA006F5CBC /* OTelSpanBuilder.swift */, + 3C6C7FE22B459AAA006F5CBC /* OTelTraceId+Datadog.swift */, + 3C6C7FE42B459AAA006F5CBC /* OTelSpanId+Datadog.swift */, + ); + path = OpenTelemetry; sourceTree = ""; }; - 61133BA62423979B00786299 /* Persistence */ = { + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */ = { isa = PBXGroup; children = ( - 61AD4E3724531500006E34EA /* DataFormat.swift */, - 61133BA92423979B00786299 /* FilesOrchestrator.swift */, - 61133BA72423979B00786299 /* FileWriter.swift */, - 61133BAD2423979B00786299 /* FileReader.swift */, - 61133BAA2423979B00786299 /* Files */, - ); - path = Persistence; + 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */, + 3C32359F2B55387A000B4258 /* OTelSpanLinkTests.swift */, + 3CC6AD1A2B4F07E700015B18 /* OTelAttributeValue+DatadogTests.swift */, + 3C6C7FF22B459AB3006F5CBC /* OTelSpanId+DatadogTests.swift */, + 3C6C7FF32B459AB3006F5CBC /* OTelTraceId+DatadogTests.swift */, + 3C6C7FF42B459AB3006F5CBC /* OTelSpanTests.swift */, + ); + path = OpenTelemetry; sourceTree = ""; }; - 61133BAA2423979B00786299 /* Files */ = { + 3CE11A3B29F7BEE700202522 /* DatadogWebViewTracking */ = { isa = PBXGroup; children = ( - 61133BAB2423979B00786299 /* Directory.swift */, - 61133BAC2423979B00786299 /* File.swift */, + 6174D6072BFCCDA400EC7469 /* ObjC */, + 3C85D41429F7C59C00AFF894 /* WebViewTracking.swift */, + D29732462A5C108700827599 /* DDScriptMessageHandler.swift */, + D29732472A5C108700827599 /* MessageEmitter.swift */, ); - path = Files; + name = DatadogWebViewTracking; + path = ../DatadogWebViewTracking/Sources; sourceTree = ""; }; - 61133BAE2423979B00786299 /* Upload */ = { + 3CE11A3C29F7BEF300202522 /* DatadogWebViewTrackingTests */ = { isa = PBXGroup; children = ( - 61133BAF2423979B00786299 /* DataUploadConditions.swift */, - 61133BB32423979B00786299 /* DataUploadDelay.swift */, - 61133BB02423979B00786299 /* DataUploader.swift */, - 61133BB12423979B00786299 /* DataUploadWorker.swift */, - 61133BB22423979B00786299 /* HTTPClient.swift */, - 61133BB42423979B00786299 /* HTTPHeaders.swift */, + D297324F2A5C109A00827599 /* MessageEmitterTests.swift */, + D29732502A5C109A00827599 /* WebViewTrackingTests.swift */, + D27CBD992BB5DBBB00C766AA /* Mocks.swift */, ); - path = Upload; + name = DatadogWebViewTrackingTests; + path = ../DatadogWebViewTracking/Tests; sourceTree = ""; }; - 61133BB72423979B00786299 /* Utils */ = { + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */ = { isa = PBXGroup; children = ( - 61C3638424361E9200C4D4E6 /* Globals.swift */, - 61133BB82423979B00786299 /* InternalLoggers.swift */, - 61133BB92423979B00786299 /* CompilationConditions.swift */, - 61133BBA2423979B00786299 /* SwiftExtensions.swift */, + 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */, + 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */, + 3CEC57752C16FDD30042B5F2 /* WatchdogTerminationAppStateManagerTests.swift */, + 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */, ); - path = Utils; + path = WatchdogTerminations; sourceTree = ""; }; - 61133BBC2423979B00786299 /* Logging */ = { + 61020C272757AD63005EEAEA /* BackgroundEvents */ = { isa = PBXGroup; children = ( - 612983CC2449E62E00D4424B /* LoggingFeature.swift */, - 61133BC12423979B00786299 /* Log */, - 61133BC52423979B00786299 /* LogOutputs */, + 61020C2B2758E853005EEAEA /* DebugBackgroundEventsViewController.swift */, + 61020C292757AD91005EEAEA /* BackgroundLocationMonitor.swift */, ); - path = Logging; + path = BackgroundEvents; sourceTree = ""; }; - 61133BBF2423979B00786299 /* Attributes */ = { + 61054E012A6EE0A400AAA894 /* DatadogSessionReplay */ = { isa = PBXGroup; children = ( - 61133BC02423979B00786299 /* UserInfo.swift */, - ); - path = Attributes; + 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 */, + 61054E132A6EE10A00AAA894 /* Utilities */, + A7B932F62B1F6A0A00AE6477 /* Models */, + 61054E032A6EE10A00AAA894 /* Writers */, + ); + name = DatadogSessionReplay; + path = ../DatadogSessionReplay/Sources; sourceTree = ""; }; - 61133BC12423979B00786299 /* Log */ = { + 61054E022A6EE0DB00AAA894 /* DatadogSessionReplayTests */ = { isa = PBXGroup; children = ( - 61133BC22423979B00786299 /* LogEncoder.swift */, - 61133BC32423979B00786299 /* LogBuilder.swift */, - 61133BC42423979B00786299 /* LogSanitizer.swift */, - ); - path = Log; + 960B26C12D03611400D7196F /* Resources */, + 96E863712C9C547B0023BF78 /* SessionReplayOverrideTests.swift */, + 61054F482A6EE1B900AAA894 /* SessionReplayTests.swift */, + 61054F3D2A6EE1B900AAA894 /* SessionReplayConfigurationTests.swift */, + D2A434AD2A8E426C0028E329 /* DDSessionReplayTests.swift */, + 96E863752C9C7E800023BF78 /* DDSessionReplayOverridesTests.swift */, + 61054F882A6EE1BA00AAA894 /* Feature */, + 61054F922A6EE1BA00AAA894 /* Helpers */, + 61054F7D2A6EE1BA00AAA894 /* Mocks */, + 61054F4E2A6EE1BA00AAA894 /* Processor */, + 61054F592A6EE1BA00AAA894 /* Recorder */, + 61054F3E2A6EE1B900AAA894 /* Utilities */, + 61054F492A6EE1BA00AAA894 /* Writer */, + ); + name = DatadogSessionReplayTests; + path = ../DatadogSessionReplay/Tests; sourceTree = ""; }; - 61133BC52423979B00786299 /* LogOutputs */ = { + 61054E032A6EE10A00AAA894 /* Writers */ = { isa = PBXGroup; children = ( - 61133BC62423979B00786299 /* LogUtilityOutputs.swift */, - 61133BC72423979B00786299 /* LogFileOutput.swift */, - 61133BC82423979B00786299 /* LogOutput.swift */, - 61133BC92423979B00786299 /* LogConsoleOutput.swift */, + 61054E082A6EE10A00AAA894 /* SRCompression.swift */, + 61054E092A6EE10A00AAA894 /* RecordWriter.swift */, + A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */, ); - path = LogOutputs; + path = Writers; sourceTree = ""; }; - 61133BF1242397DA00786299 /* DatadogObjc */ = { + 61054E0D2A6EE10A00AAA894 /* Recorder */ = { isa = PBXGroup; children = ( - 61133BF2242397DA00786299 /* DatadogObjc.h */, - 61133BF3242397DA00786299 /* Info.plist */, - ); - path = DatadogObjc; + 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */, + 61054E112A6EE10A00AAA894 /* Recorder.swift */, + 61054E122A6EE10A00AAA894 /* PrivacyLevel.swift */, + 61054E0E2A6EE10A00AAA894 /* WindowObserver */, + 61054E1A2A6EE10A00AAA894 /* TouchSnapshotProducer */, + 61054E212A6EE10A00AAA894 /* ViewTreeSnapshotProducer */, + 61054E542A6EE10A00AAA894 /* Utilities */, + ); + path = Recorder; sourceTree = ""; }; - 61133C07242397F200786299 /* TargetSupport */ = { + 61054E0E2A6EE10A00AAA894 /* WindowObserver */ = { isa = PBXGroup; children = ( - 615519242461BCE7002A85CF /* xcconfigs */, - 61133B84242393DE00786299 /* Datadog */, - 61133BF1242397DA00786299 /* DatadogObjc */, - 61133B8F242393DE00786299 /* DatadogTests */, - 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */, - 9EF49F1524476FBD004F2CA0 /* DatadogIntegrationTests */, - 61441C9E2461AF4D003D8BB8 /* Example */, + 61054E0F2A6EE10A00AAA894 /* AppWindowObserver.swift */, + 61054E102A6EE10A00AAA894 /* KeyWindowObserver.swift */, ); - path = TargetSupport; + path = WindowObserver; sourceTree = ""; }; - 61133C082423983800786299 /* DatadogObjc */ = { + 61054E132A6EE10A00AAA894 /* Utilities */ = { isa = PBXGroup; children = ( - 61133C092423983800786299 /* Datadog+objc.swift */, - 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */, - 61133C0C2423983800786299 /* Logger+objc.swift */, - 615A4A8224A3431600233986 /* Tracer+objc.swift */, - 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */, - 6132BF4524A498B400D7BD17 /* Tracing */, - 61133C0A2423983800786299 /* ObjcIntercompatibility */, - 6132BF4024A38D0600D7BD17 /* OpenTracing */, - ); - name = DatadogObjc; - path = ../Sources/DatadogObjc; + 61054E142A6EE10A00AAA894 /* UIImage+SessionReplay.swift */, + 962D72BE2CF7538800F86EF0 /* CGImage+SessionReplay.swift */, + D22442C42CA301DA002E71E4 /* UIColor+SessionReplay.swift */, + 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, + 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, + 61054E172A6EE10A00AAA894 /* SystemColors.swift */, + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */, + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */, + ); + name = Utilities; + path = Recorder/Utilities; sourceTree = ""; }; - 61133C0A2423983800786299 /* ObjcIntercompatibility */ = { + 61054E1A2A6EE10A00AAA894 /* TouchSnapshotProducer */ = { isa = PBXGroup; children = ( - 61133C0B2423983800786299 /* AnyEncodable.swift */, + 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */, + 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */, + 61054E1D2A6EE10A00AAA894 /* TouchSnapshot */, + 61054E202A6EE10A00AAA894 /* WindowTouchSnapshotProducer.swift */, ); - path = ObjcIntercompatibility; + path = TouchSnapshotProducer; sourceTree = ""; }; - 61133C122423990D00786299 /* DatadogTests */ = { + 61054E1D2A6EE10A00AAA894 /* TouchSnapshot */ = { isa = PBXGroup; children = ( - 61133C182423990D00786299 /* Datadog */, - 61133C132423990D00786299 /* DatadogObjc */, - 61C3637E2436163400C4D4E6 /* DatadogPrivate */, - 61E909F424A32CF6005EA2DE /* OpenTracing */, - 61133C422423990D00786299 /* Matchers */, - 61133C442423990D00786299 /* Helpers */, + 61054E1E2A6EE10A00AAA894 /* TouchSnapshot.swift */, + 61054E1F2A6EE10A00AAA894 /* TouchIdentifierGenerator.swift */, ); - name = DatadogTests; - path = ../Tests/DatadogTests; + path = TouchSnapshot; sourceTree = ""; }; - 61133C132423990D00786299 /* DatadogObjc */ = { + 61054E212A6EE10A00AAA894 /* ViewTreeSnapshotProducer */ = { isa = PBXGroup; children = ( - 61133C142423990D00786299 /* DDDatadogTests.swift */, - 61133C162423990D00786299 /* DDConfigurationTests.swift */, - 61133C172423990D00786299 /* DDLoggerTests.swift */, - 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */, - 615A4A8824A34FD700233986 /* DDTracerTests.swift */, - 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */, + 61054E222A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift */, + 61054E232A6EE10A00AAA894 /* ViewTreeSnapshot */, + 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */, ); - path = DatadogObjc; + path = ViewTreeSnapshotProducer; sourceTree = ""; }; - 61133C182423990D00786299 /* Datadog */ = { + 61054E232A6EE10A00AAA894 /* ViewTreeSnapshot */ = { isa = PBXGroup; children = ( - 61133C192423990D00786299 /* Mocks */, - 61133C412423990D00786299 /* DatadogTests.swift */, - 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */, - 61133C382423990D00786299 /* LoggerTests.swift */, - 61C5A89524509BF600DA608C /* TracerTests.swift */, - 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */, - 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */, - 61133C212423990D00786299 /* Core */, - 61133C392423990D00786299 /* Logging */, - 61C5A89724509C1100DA608C /* Tracing */, - 61216278247D20D500AC5D67 /* FeaturesIntegration */, - 61133C352423990D00786299 /* Utils */, - ); - path = Datadog; + 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */, + 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */, + 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */, + 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */, + 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */, + 61054E272A6EE10A00AAA894 /* NodeRecorders */, + ); + path = ViewTreeSnapshot; sourceTree = ""; }; - 61133C192423990D00786299 /* Mocks */ = { + 61054E272A6EE10A00AAA894 /* NodeRecorders */ = { isa = PBXGroup; children = ( - 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, - 61F1A6192498A51700075390 /* CoreMocks.swift */, - 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */, - 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, - 61F1A61B2498AD2C00075390 /* SystemFrameworks */, - ); - path = Mocks; + D2AD1CC12CE4AE6600106C74 /* SwiftUI */, + 969B3B202C33F80500D62400 /* UIActivityIndicatorRecorder.swift */, + 61054E282A6EE10A00AAA894 /* UIDatePickerRecorder.swift */, + 61054E292A6EE10A00AAA894 /* UITextViewRecorder.swift */, + 61054E2A2A6EE10A00AAA894 /* UIImageViewRecorder.swift */, + A70ADCD12B583B1300321BC9 /* UIImageResource.swift */, + 61054E2B2A6EE10A00AAA894 /* UIViewRecorder.swift */, + 61054E2C2A6EE10A00AAA894 /* UINavigationBarRecorder.swift */, + 61054E2D2A6EE10A00AAA894 /* UITextFieldRecorder.swift */, + 61054E2E2A6EE10A00AAA894 /* NodeRecorder.swift */, + 61054E2F2A6EE10A00AAA894 /* UISliderRecorder.swift */, + 61054E302A6EE10A00AAA894 /* UIPickerViewRecorder.swift */, + 96E414132C2AF56F005A6119 /* UIProgressViewRecorder.swift */, + 61054E312A6EE10A00AAA894 /* UIStepperRecorder.swift */, + 61054E322A6EE10A00AAA894 /* UILabelRecorder.swift */, + 61054E332A6EE10A00AAA894 /* UISwitchRecorder.swift */, + 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */, + 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */, + 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */, + D2BCB2A02B7B8107005C2AAB /* WKWebViewRecorder.swift */, + D2AD1CCB2CE4AE9800106C74 /* UIHostingViewRecorder.swift */, + ); + path = NodeRecorders; sourceTree = ""; }; - 61133C212423990D00786299 /* Core */ = { + 61054E3B2A6EE10A00AAA894 /* Feature */ = { isa = PBXGroup; children = ( - 61345612244756E300E7DA6B /* PerformancePresetTests.swift */, - 618C365D248E858200520CDE /* Utils */, - 9E5D2D4B2491382800763FE4 /* AutoInstrumentation */, - 61133C222423990D00786299 /* System */, - 61133C272423990D00786299 /* Persistence */, - 61133C2E2423990D00786299 /* Upload */, - 61E917CD246426E000E6C631 /* Utils */, - ); - path = Core; + A73A54972B16406900E1F7E3 /* ResourcesFeature.swift */, + 61054E3C2A6EE10A00AAA894 /* SessionReplayFeature.swift */, + 61054E3E2A6EE10A00AAA894 /* RUMContextReceiver.swift */, + D2C5D52A2B84F6AB00B63F36 /* WebViewRecordReceiver.swift */, + 61054E3F2A6EE10A00AAA894 /* SRContextPublisher.swift */, + D22C5BCD2A98A65D0024CC1F /* Baggages.swift */, + D218B0452D072C8400E3F82C /* SessionReplayTelemetry.swift */, + 61054E402A6EE10A00AAA894 /* RequestBuilders */, + ); + path = Feature; sourceTree = ""; }; - 61133C222423990D00786299 /* System */ = { + 61054E402A6EE10A00AAA894 /* RequestBuilders */ = { isa = PBXGroup; children = ( - 61133C232423990D00786299 /* MobileDeviceTests.swift */, - 61133C242423990D00786299 /* NetworkConnectionInfoProviderTests.swift */, - 61133C252423990D00786299 /* BatteryStatusProviderTests.swift */, - 61133C262423990D00786299 /* CarrierInfoProviderTests.swift */, + A74A72802B0CEE4900771FEB /* ResourceRequestBuilder.swift */, + 61054E412A6EE10A00AAA894 /* SegmentRequestBuilder.swift */, + 61054E422A6EE10A00AAA894 /* JSON */, + 61054E462A6EE10A00AAA894 /* Multipart */, ); - path = System; + path = RequestBuilders; sourceTree = ""; }; - 61133C272423990D00786299 /* Persistence */ = { + 61054E422A6EE10A00AAA894 /* JSON */ = { isa = PBXGroup; children = ( - 61133C2A2423990D00786299 /* FilesOrchestratorTests.swift */, - 61133C292423990D00786299 /* FileWriterTests.swift */, - 61133C282423990D00786299 /* FileReaderTests.swift */, - 61133C2B2423990D00786299 /* Files */, + 61054E432A6EE10A00AAA894 /* SegmentJSON.swift */, ); - path = Persistence; + path = JSON; sourceTree = ""; }; - 61133C2B2423990D00786299 /* Files */ = { + 61054E462A6EE10A00AAA894 /* Multipart */ = { isa = PBXGroup; children = ( - 61133C2C2423990D00786299 /* FileTests.swift */, - 61133C2D2423990D00786299 /* DirectoryTests.swift */, + 61054E472A6EE10A00AAA894 /* MultipartFormData.swift */, ); - path = Files; + path = Multipart; sourceTree = ""; }; - 61133C2E2423990D00786299 /* Upload */ = { + 61054E482A6EE10A00AAA894 /* Processor */ = { isa = PBXGroup; children = ( - 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */, - 61133C302423990D00786299 /* DataUploadConditionsTests.swift */, - 61133C312423990D00786299 /* LogsUploadDelayTests.swift */, - 61133C322423990D00786299 /* DataUploaderTests.swift */, - 61133C332423990D00786299 /* HTTPHeadersTests.swift */, - 61133C342423990D00786299 /* HTTPClientTests.swift */, - ); - path = Upload; + 61054E4B2A6EE10A00AAA894 /* SnapshotProcessor.swift */, + A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */, + 61054E492A6EE10A00AAA894 /* Privacy */, + 61054E4C2A6EE10A00AAA894 /* Diffing */, + 61054E4F2A6EE10A00AAA894 /* Builders */, + 61054E522A6EE10A00AAA894 /* Flattening */, + ); + path = Processor; sourceTree = ""; }; - 61133C352423990D00786299 /* Utils */ = { + 61054E492A6EE10A00AAA894 /* Privacy */ = { isa = PBXGroup; children = ( - 61133C362423990D00786299 /* InternalLoggersTests.swift */, - 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, + 61054E4A2A6EE10A00AAA894 /* TextObfuscator.swift */, ); - path = Utils; + path = Privacy; sourceTree = ""; }; - 61133C392423990D00786299 /* Logging */ = { + 61054E4C2A6EE10A00AAA894 /* Diffing */ = { isa = PBXGroup; children = ( - 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */, - 61133C3A2423990D00786299 /* Log */, - 61133C3D2423990D00786299 /* LogOutputs */, + 61054E4D2A6EE10A00AAA894 /* Diff+SRWireframes.swift */, + 61054E4E2A6EE10A00AAA894 /* Diff.swift */, ); - path = Logging; + path = Diffing; sourceTree = ""; }; - 61133C3A2423990D00786299 /* Log */ = { + 61054E4F2A6EE10A00AAA894 /* Builders */ = { isa = PBXGroup; children = ( - 61133C3B2423990D00786299 /* LogBuilderTests.swift */, - 61133C3C2423990D00786299 /* LogSanitizerTests.swift */, + 61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */, + 61054E512A6EE10A00AAA894 /* WireframesBuilder.swift */, ); - path = Log; + path = Builders; sourceTree = ""; }; - 61133C3D2423990D00786299 /* LogOutputs */ = { + 61054E522A6EE10A00AAA894 /* Flattening */ = { isa = PBXGroup; children = ( - 61133C3E2423990D00786299 /* LogConsoleOutputTests.swift */, - 61133C3F2423990D00786299 /* LogUtilityOutputsTests.swift */, - 61133C402423990D00786299 /* LogFileOutputTests.swift */, + 61054E532A6EE10A00AAA894 /* NodesFlattener.swift */, ); - path = LogOutputs; + path = Flattening; sourceTree = ""; }; - 61133C422423990D00786299 /* Matchers */ = { + 61054E542A6EE10A00AAA894 /* Utilities */ = { isa = PBXGroup; children = ( - 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */, - 61133C432423990D00786299 /* LogMatcher.swift */, - 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */, + 61054E552A6EE10A00AAA894 /* CGRectExtensions.swift */, + 61054E582A6EE10A00AAA894 /* Queue.swift */, + D29C9F682D00739400CD568E /* Reflector.swift */, + D2EA0F422C0D941900CB20F8 /* ReflectionMirror.swift */, + 61054E592A6EE10A00AAA894 /* Errors.swift */, + 61054E5A2A6EE10A00AAA894 /* Colors.swift */, + 61054E5B2A6EE10A00AAA894 /* Schedulers */, + ); + name = Utilities; + path = ../Utilities; + sourceTree = ""; + }; + 61054E5B2A6EE10A00AAA894 /* Schedulers */ = { + isa = PBXGroup; + children = ( + 61054E5C2A6EE10A00AAA894 /* MainThreadScheduler.swift */, + 61054E5D2A6EE10A00AAA894 /* Scheduler.swift */, ); - path = Matchers; + path = Schedulers; sourceTree = ""; }; - 61133C442423990D00786299 /* Helpers */ = { + 61054F3E2A6EE1B900AAA894 /* Utilities */ = { isa = PBXGroup; children = ( - 61133C452423990D00786299 /* SwiftExtensions.swift */, - 61133C462423990D00786299 /* TestsDirectory.swift */, - 61133C472423990D00786299 /* DatadogExtensions.swift */, - 61F1A622249B811200075390 /* Encoding.swift */, + 61054F402A6EE1B900AAA894 /* UIImage+SessionReplayTests.swift */, + 61054F412A6EE1B900AAA894 /* CGRectExtensionsTests.swift */, + 61054F422A6EE1B900AAA894 /* ColorsTests.swift */, + 61054F432A6EE1B900AAA894 /* CFType+SafetyTests.swift */, + 61054F442A6EE1B900AAA894 /* QueueTests.swift */, + 61054F452A6EE1B900AAA894 /* SwiftExtensionsTests.swift */, + 61054F462A6EE1B900AAA894 /* Schedulers */, + ); + path = Utilities; + sourceTree = ""; + }; + 61054F462A6EE1B900AAA894 /* Schedulers */ = { + isa = PBXGroup; + children = ( + 61054F472A6EE1B900AAA894 /* MainThreadSchedulerTests.swift */, ); - path = Helpers; + path = Schedulers; sourceTree = ""; }; - 61133C6F2423993200786299 /* Frameworks */ = { + 61054F492A6EE1BA00AAA894 /* Writer */ = { isa = PBXGroup; children = ( - 61C5A8732450989E00DA608C /* OpenTracing.framework */, + A71013D52B178FAD00101E60 /* ResourcesWriterTests.swift */, + 61054F4A2A6EE1BA00AAA894 /* RecordsWriterTests.swift */, + 61054F4B2A6EE1BA00AAA894 /* SRCompressionTests.swift */, ); - name = Frameworks; + path = Writer; sourceTree = ""; }; - 61216277247D1F2100AC5D67 /* FeaturesIntegration */ = { + 61054F4E2A6EE1BA00AAA894 /* Processor */ = { + isa = PBXGroup; + children = ( + 61054F4F2A6EE1BA00AAA894 /* Privacy */, + 61054F512A6EE1BA00AAA894 /* Diffing */, + 61054F542A6EE1BA00AAA894 /* Builders */, + 61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */, + A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */, + 61054F572A6EE1BA00AAA894 /* Flattening */, + ); + path = Processor; + sourceTree = ""; + }; + 61054F4F2A6EE1BA00AAA894 /* Privacy */ = { isa = PBXGroup; children = ( - 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */, + 61054F502A6EE1BA00AAA894 /* TextObfuscatorTests.swift */, ); - path = FeaturesIntegration; + path = Privacy; sourceTree = ""; }; - 61216278247D20D500AC5D67 /* FeaturesIntegration */ = { + 61054F512A6EE1BA00AAA894 /* Diffing */ = { isa = PBXGroup; children = ( - 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */, + 61054F522A6EE1BA00AAA894 /* Diff+SRWireframesTests.swift */, + 61054F532A6EE1BA00AAA894 /* DiffTests.swift */, ); - path = FeaturesIntegration; + path = Diffing; sourceTree = ""; }; - 6132BF4024A38D0600D7BD17 /* OpenTracing */ = { + 61054F542A6EE1BA00AAA894 /* Builders */ = { isa = PBXGroup; children = ( - 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */, - 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */, - 615A4A8A24A3568900233986 /* OTSpan+objc.swift */, - 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */, - 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */, + 61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */, + D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */, ); - path = OpenTracing; + path = Builders; sourceTree = ""; }; - 6132BF4524A498B400D7BD17 /* Tracing */ = { + 61054F572A6EE1BA00AAA894 /* Flattening */ = { isa = PBXGroup; children = ( - 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */, - 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */, - 6132BF4A24A49C7200D7BD17 /* Propagation */, - 6132BF4F24A49F6400D7BD17 /* Utils */, + 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */, ); - path = Tracing; + path = Flattening; sourceTree = ""; }; - 6132BF4A24A49C7200D7BD17 /* Propagation */ = { + 61054F592A6EE1BA00AAA894 /* Recorder */ = { isa = PBXGroup; children = ( - 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, + 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */, + 61054F5B2A6EE1BA00AAA894 /* Utilties */, + 61054F602A6EE1BA00AAA894 /* TouchSnapshotProducer */, + 61054F642A6EE1BA00AAA894 /* ViewTreeSnapshotProducer */, + 61054F7B2A6EE1BA00AAA894 /* TextAndInputPrivacyLevelTests.swift */, + 61054F7C2A6EE1BA00AAA894 /* RecorderTests.swift */, + ); + path = Recorder; + sourceTree = ""; + }; + 61054F5B2A6EE1BA00AAA894 /* Utilties */ = { + isa = PBXGroup; + children = ( + 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */, + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */, + D2AD1CCE2CE4AEF600106C74 /* ReflectionMirrorTests.swift */, + D29C9F6A2D01D5F600CD568E /* ReflectorTests.swift */, ); - path = Propagation; + path = Utilties; sourceTree = ""; }; - 6132BF4F24A49F6400D7BD17 /* Utils */ = { + 61054F602A6EE1BA00AAA894 /* TouchSnapshotProducer */ = { isa = PBXGroup; children = ( - 6132BF5024A49F7400D7BD17 /* Casting.swift */, + 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */, + 61054F622A6EE1BA00AAA894 /* TouchSnapshot */, ); - path = Utils; + path = TouchSnapshotProducer; sourceTree = ""; }; - 61441C0324616DE9003D8BB8 /* Example */ = { + 61054F622A6EE1BA00AAA894 /* TouchSnapshot */ = { isa = PBXGroup; children = ( - 61441C9C2461A796003D8BB8 /* AppConfig.swift */, - 61441C0424616DE9003D8BB8 /* AppDelegate.swift */, - 61441C9A2461A64F003D8BB8 /* Debugging */, - 61B9ED142461DFEE00C0DCFF /* IntegrationTestFixtures */, - 61441C8F2461A648003D8BB8 /* Utils */, - 61441C0A24616DE9003D8BB8 /* Main.storyboard */, - 61441C0D24616DEC003D8BB8 /* Assets.xcassets */, - 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */, + 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */, ); - path = Example; + path = TouchSnapshot; sourceTree = ""; }; - 61441C3524617013003D8BB8 /* DatadogIntegrationTests */ = { + 61054F642A6EE1BA00AAA894 /* ViewTreeSnapshotProducer */ = { isa = PBXGroup; children = ( - 61441C3B24617013003D8BB8 /* IntegrationTests.swift */, - 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */, - 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */, - 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */, + 61054F652A6EE1BA00AAA894 /* ViewTreeSnapshot */, ); - name = DatadogIntegrationTests; - path = ../Tests/DatadogIntegrationTests; + path = ViewTreeSnapshotProducer; + sourceTree = ""; + }; + 61054F652A6EE1BA00AAA894 /* ViewTreeSnapshot */ = { + isa = PBXGroup; + children = ( + 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */, + 61054F672A6EE1BA00AAA894 /* ViewTreeSnapshotBuilderTests.swift */, + 61054F682A6EE1BA00AAA894 /* NodeIDGeneratorTests.swift */, + 61054F692A6EE1BA00AAA894 /* NodeRecorders */, + 61054F792A6EE1BA00AAA894 /* ViewTreeRecorderTests.swift */, + 61054F7A2A6EE1BA00AAA894 /* ViewTreeSnapshotTests.swift */, + ); + path = ViewTreeSnapshot; + sourceTree = ""; + }; + 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 */, + 61054F702A6EE1BA00AAA894 /* UIDatePickerRecorderTests.swift */, + 61054F712A6EE1BA00AAA894 /* UINavigationBarRecorderTests.swift */, + 61054F722A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift */, + 61054F732A6EE1BA00AAA894 /* UISwitchRecorderTests.swift */, + 61054F742A6EE1BA00AAA894 /* UIStepperRecorderTests.swift */, + 61054F752A6EE1BA00AAA894 /* UIViewRecorderTests.swift */, + 61054F762A6EE1BA00AAA894 /* UIImageViewWireframesBuilderTests.swift */, + 61054F772A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift */, + 61054F782A6EE1BA00AAA894 /* UITextViewRecorderTests.swift */, + D2BCB2A22B7B9683005C2AAB /* WKWebViewRecorderTests.swift */, + A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */, + D28ABFD22CEB87C600623F27 /* UIHostingViewRecorderTests.swift */, + ); + path = NodeRecorders; sourceTree = ""; }; - 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */ = { + 61054F7D2A6EE1BA00AAA894 /* Mocks */ = { isa = PBXGroup; children = ( - 61441C6C24619FE4003D8BB8 /* Info.plist */, + D2AE9A5C2CF8836D00695264 /* FeatureFlagsMock.swift */, + 96F25A842CC7EB3700459567 /* PrivacyOverridesMock+objc.swift */, + 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */, + 61054F7E2A6EE1BA00AAA894 /* UIKitMocks.swift */, + 61054F7F2A6EE1BA00AAA894 /* CoreGraphicsMocks.swift */, + 61054F802A6EE1BA00AAA894 /* SRDataModelsMocks.swift */, + 61054F812A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift */, + 61054F822A6EE1BA00AAA894 /* RecorderMocks.swift */, + 61054F832A6EE1BA00AAA894 /* TestScheduler.swift */, + 61054F842A6EE1BA00AAA894 /* QueueMocks.swift */, + 61054F862A6EE1BA00AAA894 /* SnapshotProducerMocks.swift */, + 61054F872A6EE1BA00AAA894 /* RUMContextObserverMock.swift */, + A74A72862B10CE4100771FEB /* ResourceMocks.swift */, + A74A72882B10D95D00771FEB /* MultipartBuilderSpy.swift */, + A7D952892B28BD94004C79B1 /* ResourceProcessorSpy.swift */, + 962D72C62CF7815300F86EF0 /* ReflectionMocks.swift */, ); - path = DatadogBenchmarkTests; + path = Mocks; sourceTree = ""; }; - 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */ = { + 61054F882A6EE1BA00AAA894 /* Feature */ = { isa = PBXGroup; children = ( - 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */, - 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */, + 61054F892A6EE1BA00AAA894 /* RUMContextReceiverTests.swift */, + 61054F8A2A6EE1BA00AAA894 /* SRContextPublisherTests.swift */, + D2C5D52C2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift */, + D218B0472D072CF300E3F82C /* SessionReplayTelemetryTests.swift */, + 61054F8B2A6EE1BA00AAA894 /* RequestBuilder */, ); - name = DatadogBenchmarkTests; - path = ../Tests/DatadogBenchmarkTests; + path = Feature; sourceTree = ""; }; - 61441C8F2461A648003D8BB8 /* Utils */ = { + 61054F8B2A6EE1BA00AAA894 /* RequestBuilder */ = { isa = PBXGroup; children = ( - 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */, - 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */, - 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */, + 61054F8C2A6EE1BA00AAA894 /* JSON */, + 61054F8F2A6EE1BA00AAA894 /* Multipart */, + 61054F912A6EE1BA00AAA894 /* SegmentRequestBuilderTests.swift */, + A74A72842B10CC6700771FEB /* ResourceRequestBuilderTests.swift */, ); - path = Utils; + path = RequestBuilder; sourceTree = ""; }; - 61441C9A2461A64F003D8BB8 /* Debugging */ = { + 61054F8C2A6EE1BA00AAA894 /* JSON */ = { isa = PBXGroup; children = ( - 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */, - 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */, + D25C834B2B8657CF008E73B1 /* SegmentJSONTests.swift */, ); - path = Debugging; + path = JSON; sourceTree = ""; }; - 61441C9E2461AF4D003D8BB8 /* Example */ = { + 61054F8F2A6EE1BA00AAA894 /* Multipart */ = { isa = PBXGroup; children = ( - 61441C1224616DEC003D8BB8 /* Info.plist */, + 61054F902A6EE1BA00AAA894 /* MultipartFormDataTests.swift */, ); - path = Example; + path = Multipart; sourceTree = ""; }; - 615519242461BCE7002A85CF /* xcconfigs */ = { + 61054F922A6EE1BA00AAA894 /* Helpers */ = { isa = PBXGroup; children = ( - 615519252461BCE7002A85CF /* Datadog.xcconfig */, - 615519262461BCE7002A85CF /* Datadog.local.xcconfig */, + 61054F932A6EE1BA00AAA894 /* XCTAssertRectsEqual.swift */, ); - name = xcconfigs; - path = ../../xcconfigs; + path = Helpers; + sourceTree = ""; + }; + 610ABD492A69309900AFEA34 /* IntegrationUnitTests */ = { + isa = PBXGroup; + children = ( + D2C5D52E2B84F6E700B63F36 /* SessionReplay */, + 6179DB542B60229D00E9E04E /* CrashReporting */, + 61E8C5062B28896100E709B4 /* RUM */, + 61F3E36B2BC7D51400C7881E /* Trace */, + 610ABD4A2A6930AB00AFEA34 /* Public */, + 618353BA2A6946F40085F84A /* Internal */, + ); + path = IntegrationUnitTests; sourceTree = ""; }; - 617CEB372456BC2200AD4669 /* UUIDs */ = { + 610ABD4A2A6930AB00AFEA34 /* Public */ = { isa = PBXGroup; children = ( - 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */, - 617CEB382456BC3A00AD4669 /* TracingUUID.swift */, + 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */, + 610ABD4B2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift */, + D20FD9D52ACC0934004D3569 /* WebLogIntegrationTests.swift */, + D21831542B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift */, ); - path = UUIDs; + path = Public; sourceTree = ""; }; - 617CEB3A2456BC8200AD4669 /* UUIDs */ = { + 6111543425C993AC007C84C9 /* CrashReporting */ = { isa = PBXGroup; children = ( - 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */, - 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */, + 617247AD25DA9BEA007085B3 /* CrashReportingObjcHelpers.h */, + 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */, ); - path = UUIDs; + path = CrashReporting; sourceTree = ""; }; - 618C365D248E858200520CDE /* Utils */ = { + 6111C58025C0080C00F5C4A2 /* RUM */ = { isa = PBXGroup; children = ( - 618C365E248E85B400520CDE /* DateFormattingTests.swift */, - 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */, + 9E55407B25812D1C00F6E3AD /* RUM+objc.swift */, + 6111C58125C0081F00F5C4A2 /* RUMDataModels+objc.swift */, ); - path = Utils; + path = RUM; sourceTree = ""; }; - 61B9ED142461DFEE00C0DCFF /* IntegrationTestFixtures */ = { + 61133B78242393DE00786299 = { isa = PBXGroup; children = ( - 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */, - 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */, + 61133B9C2423979B00786299 /* DatadogCore */, + 9E68FB52244707FD0013A8AA /* DatadogPrivate */, + 610ABD492A69309900AFEA34 /* IntegrationUnitTests */, + 61133C122423990D00786299 /* DatadogCoreTests */, + 61133C082423983800786299 /* DatadogObjc */, + D23039A6298D513D001A1FA3 /* DatadogInternal */, + D2DA238B298D588A00C6C7E6 /* DatadogInternalTests */, + D207317D29A5226A00ECBF94 /* DatadogLogs */, + D207318729A5226B00ECBF94 /* DatadogLogsTests */, + D25EE93529C4C3C300CE3839 /* DatadogTrace */, + D25EE93F29C4C3C400CE3839 /* DatadogTraceTests */, + D29A9F3529DD84AA005C54A4 /* DatadogRUM */, + D29A9F3F29DD84AB005C54A4 /* DatadogRUMTests */, + 61054E012A6EE0A400AAA894 /* DatadogSessionReplay */, + 61054E022A6EE0DB00AAA894 /* DatadogSessionReplayTests */, + 6170DC1325C1864B003AED5C /* DatadogCrashReporting */, + 6170DC1425C18663003AED5C /* DatadogCrashReportingTests */, + 3CE11A3B29F7BEE700202522 /* DatadogWebViewTracking */, + 3CE11A3C29F7BEF300202522 /* DatadogWebViewTrackingTests */, + 61133C07242397F200786299 /* TargetSupport */, + 61441C0324616DE9003D8BB8 /* Example */, + 6199362C265BA959009D7EA8 /* E2E */, + 61993666265BBEDC009D7EA8 /* E2ETests */, + 618F9841265BC486009959F8 /* E2EInstrumentationTests */, + D257953F298ABA65008A1BE5 /* TestUtilities */, + 61133B83242393DE00786299 /* Products */, + 61133C6F2423993200786299 /* Frameworks */, ); - path = IntegrationTestFixtures; sourceTree = ""; }; - 61C3637E2436163400C4D4E6 /* DatadogPrivate */ = { + 61133B83242393DE00786299 /* Products */ = { isa = PBXGroup; children = ( - 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */, + 61133B82242393DE00786299 /* DatadogCore.framework */, + 61133B8B242393DE00786299 /* DatadogCoreTests iOS.xctest */, + 61133BF0242397DA00786299 /* DatadogObjc.framework */, + 61441C0224616DE9003D8BB8 /* Example iOS.app */, + 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */, + 61B7885C25C180CB002675B5 /* DatadogCrashReportingTests iOS.xctest */, + 6199362B265BA958009D7EA8 /* E2E.app */, + 61993665265BBEDC009D7EA8 /* E2ETests.xctest */, + 618F9840265BC486009959F8 /* E2EInstrumentationTests.xctest */, + D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */, + D2CB6F8F27C520D400A62B57 /* DatadogCoreTests tvOS.xctest */, + D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */, + D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */, + D2CB6FEC27C5352300A62B57 /* DatadogCrashReportingTests tvOS.xctest */, + D240684D27CE6C9E00C04F44 /* Example tvOS.app */, + D257953E298ABA65008A1BE5 /* TestUtilities.framework */, + D257958B298ABB83008A1BE5 /* TestUtilities.framework */, + D23039A5298D513C001A1FA3 /* DatadogInternal.framework */, + D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */, + D2DA238A298D588800C6C7E6 /* DatadogInternalTests iOS.xctest */, + D2DA23C3298D59DC00C6C7E6 /* DatadogInternalTests tvOS.xctest */, + D207317C29A5226A00ECBF94 /* DatadogLogs.framework */, + D207318329A5226A00ECBF94 /* DatadogLogsTests iOS.xctest */, + D20731B429A5279D00ECBF94 /* DatadogLogs.framework */, + D2A7840129A534F9003B03BB /* DatadogLogsTests tvOS.xctest */, + D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */, + D25EE93B29C4C3C300CE3839 /* DatadogTraceTests iOS.xctest */, + D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */, + D2C1A57329C4F2E800946C31 /* DatadogTraceTests tvOS.xctest */, + D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */, + D29A9F3B29DD84AB005C54A4 /* DatadogRUMTests iOS.xctest */, + D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */, + D23F8ECD29DDCD38001CFAE8 /* DatadogRUMTests tvOS.xctest */, + 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */, + 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */, + 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */, + 6133D2082A6EDB7700384BEF /* DatadogSessionReplayTests iOS.xctest */, ); - path = DatadogPrivate; + name = Products; sourceTree = ""; }; - 61C5A87724509A0C00DA608C /* Tracing */ = { + 61133B84242393DE00786299 /* DatadogCore */ = { isa = PBXGroup; children = ( - 61C5A88F24509AA700DA608C /* TracingFeature.swift */, - 61C5A87924509A0C00DA608C /* DDNoOps.swift */, - 61C5A87824509A0C00DA608C /* DDSpan.swift */, - 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */, - 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */, - 61C5A8A324509FAA00DA608C /* Span */, - 61C5A87F24509A0C00DA608C /* SpanOutputs */, - 61C5A88224509A0C00DA608C /* Propagation */, - 617CEB372456BC2200AD4669 /* UUIDs */, - 61C5A87A24509A0C00DA608C /* Utils */, + 61133B85242393DE00786299 /* DatadogCore.h */, + 61133B86242393DE00786299 /* Info.plist */, ); - path = Tracing; + path = DatadogCore; sourceTree = ""; }; - 61C5A87A24509A0C00DA608C /* Utils */ = { + 61133B8F242393DE00786299 /* DatadogTests */ = { isa = PBXGroup; children = ( - 61C5A87C24509A0C00DA608C /* Casting.swift */, - 61C5A87D24509A0C00DA608C /* Warnings.swift */, + 61378BA72555329E00F28837 /* DatadogTests.xcconfig */, + 61133B92242393DE00786299 /* Info.plist */, + 61A763D9252DB2B3005A23F2 /* DatadogTests-Bridging-Header.h */, ); - path = Utils; + path = DatadogTests; sourceTree = ""; }; - 61C5A87F24509A0C00DA608C /* SpanOutputs */ = { + 61133B9C2423979B00786299 /* DatadogCore */ = { isa = PBXGroup; children = ( - 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */, - 61C5A88124509A0C00DA608C /* SpanOutput.swift */, + D28FCC342B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy */, + D286626D2A43487500852CE3 /* Datadog.swift */, + D2FB125C292FBB56005B13F8 /* Datadog+Internal.swift */, + E1D5AEA624B4D45A007F194B /* Versioning.swift */, + 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */, + 61133B9E2423979B00786299 /* Core */, + 61216277247D1F2100AC5D67 /* FeaturesIntegration */, + 61133BB72423979B00786299 /* Utils */, + 61D3E0C7277B237D008BE766 /* Kronos */, + 6174D6082BFDDD1E00EC7469 /* SDKMetrics */, ); - path = SpanOutputs; + name = DatadogCore; + path = ../DatadogCore/Sources; sourceTree = ""; }; - 61C5A88224509A0C00DA608C /* Propagation */ = { + 61133B9E2423979B00786299 /* Core */ = { isa = PBXGroup; children = ( - 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */, + D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */, + D214DAA429E072D7004D0AE8 /* MessageBus.swift */, + D2EFA866286DA82700F1FAA6 /* Context */, + 61133BA62423979B00786299 /* Storage */, + 6128F56C2BA223A100D35B08 /* DataStore */, + 61F930BC2BA1A405005F0EE2 /* TLV */, + D26C49B428893E5300802B2D /* Upload */, ); - path = Propagation; + path = Core; sourceTree = ""; }; - 61C5A89724509C1100DA608C /* Tracing */ = { + 61133BA62423979B00786299 /* Storage */ = { isa = PBXGroup; children = ( - 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */, - 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */, - 61C5A89824509C1100DA608C /* DDSpanTests.swift */, - 61F1A620249A45E400075390 /* DDSpanContextTests.swift */, - 61E45BD02450F64100F2C652 /* Span */, - 61E45BE3245196D500F2C652 /* SpanOutputs */, - 617CEB3A2456BC8200AD4669 /* UUIDs */, - 61C5A89924509C1100DA608C /* Utils */, + 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */, + D21C26C428A3B49C005DD405 /* FeatureStorage.swift */, + 61DA8CA828609C5B0074A606 /* Directories.swift */, + D2DC4BF527F484AA00E4FB96 /* DataEncryption.swift */, + 61133BA92423979B00786299 /* FilesOrchestrator.swift */, + 3C0D5DDC2A543D5D00446CF9 /* EventGenerator.swift */, + D2A7841429A53B92003B03BB /* Files */, + 613E79412577C08900DFCC17 /* Writing */, + 613E79422577C09B00DFCC17 /* Reading */, ); - path = Tracing; + path = Storage; sourceTree = ""; }; - 61C5A89924509C1100DA608C /* Utils */ = { + 61133BB72423979B00786299 /* Utils */ = { isa = PBXGroup; children = ( - 61C5A89B24509C1100DA608C /* UUID.swift */, - 61C5A89A24509C1100DA608C /* WarningsTests.swift */, - 61C5A89C24509C1100DA608C /* Casting.swift */, + 6139CD702589FAFD007E8BB7 /* Retrying.swift */, + 61DA8CAE28620C760074A606 /* Cryptography.swift */, ); path = Utils; sourceTree = ""; }; - 61C5A8A324509FAA00DA608C /* Span */ = { + 61133BC12423979B00786299 /* Log */ = { isa = PBXGroup; children = ( - 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */, - 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */, - 614872762485067300E3EBDB /* SpanTagsReducer.swift */, + 61133BC22423979B00786299 /* LogEventEncoder.swift */, + 61133BC32423979B00786299 /* LogEventBuilder.swift */, + 61133BC42423979B00786299 /* LogEventSanitizer.swift */, + 615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */, + 615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */, ); - path = Span; + path = Log; sourceTree = ""; }; - 61E45BD02450F64100F2C652 /* Span */ = { + 61133BF1242397DA00786299 /* DatadogObjc */ = { isa = PBXGroup; children = ( - 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */, + 61133BF2242397DA00786299 /* DatadogObjc.h */, + 61133BF3242397DA00786299 /* Info.plist */, ); - path = Span; + path = DatadogObjc; sourceTree = ""; }; - 61E45BE3245196D500F2C652 /* SpanOutputs */ = { + 61133C07242397F200786299 /* TargetSupport */ = { isa = PBXGroup; children = ( - 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */, + 615519242461BCE7002A85CF /* xcconfigs */, + 61133B84242393DE00786299 /* DatadogCore */, + 61133BF1242397DA00786299 /* DatadogObjc */, + 6170DC0525C184FA003AED5C /* DatadogCrashReporting */, + 61133B8F242393DE00786299 /* DatadogTests */, + 6170DC0625C184FA003AED5C /* DatadogCrashReportingTests */, + 61441C9E2461AF4D003D8BB8 /* Example */, + 61993640265BAC34009D7EA8 /* E2E */, + 61993670265BBF19009D7EA8 /* E2ETests */, + 618F984B265BC4C0009959F8 /* E2EInstrumentationTests */, ); - path = SpanOutputs; + path = TargetSupport; sourceTree = ""; }; - 61E909E524A24DD3005EA2DE /* OpenTracing */ = { + 61133C082423983800786299 /* DatadogObjc */ = { isa = PBXGroup; children = ( - 61E909E624A24DD3005EA2DE /* OTSpan.swift */, - 61E909E724A24DD3005EA2DE /* OTFormat.swift */, - 61E909E824A24DD3005EA2DE /* OTGlobal.swift */, - 61E909E924A24DD3005EA2DE /* OTTracer.swift */, - 61E909EA24A24DD3005EA2DE /* OTReference.swift */, - 61E909EB24A24DD3005EA2DE /* OTConstants.swift */, - 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */, + 3CCCA5C32ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift */, + 61133C092423983800786299 /* Datadog+objc.swift */, + 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */, + 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */, + D2A434A62A8E3FEA0028E329 /* Logs */, + 6132BF4524A498B400D7BD17 /* Tracing */, + 6111C58025C0080C00F5C4A2 /* RUM */, + 6132BF4024A38D0600D7BD17 /* OpenTracing */, + F603F1252CAE9F760088E6B7 /* DDInternalLogger+objc.swift */, ); - path = OpenTracing; + name = DatadogObjc; + path = ../DatadogObjc/Sources; sourceTree = ""; }; - 61E909F424A32CF6005EA2DE /* OpenTracing */ = { + 61133C122423990D00786299 /* DatadogCoreTests */ = { isa = PBXGroup; children = ( - 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */, + 6184751626EFD01600C7C9C5 /* TestsObserver */, + 61133C182423990D00786299 /* Datadog */, + 61133C132423990D00786299 /* DatadogObjc */, + 61C3637E2436163400C4D4E6 /* DatadogPrivate */, + 61133C422423990D00786299 /* Matchers */, + 61133C442423990D00786299 /* Helpers */, ); - path = OpenTracing; + name = DatadogCoreTests; + path = ../DatadogCore/Tests; sourceTree = ""; }; - 61E917CD246426E000E6C631 /* Utils */ = { + 61133C132423990D00786299 /* DatadogObjc */ = { isa = PBXGroup; children = ( - 61E917CE2464270500E6C631 /* EncodableValueTests.swift */, + 61B5E41F26DF857E000B0A5F /* ObjcAPITests */, + 61133C142423990D00786299 /* DDDatadogTests.swift */, + 61133C162423990D00786299 /* DDConfigurationTests.swift */, + 61133C172423990D00786299 /* DDLogsTests.swift */, + 614798982A459B2E0095CB02 /* DDTraceConfigurationTests.swift */, + 614798952A459AA80095CB02 /* DDTraceTests.swift */, + 615A4A8824A34FD700233986 /* DDTracerTests.swift */, + 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */, + 61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */, + 61A2CC232A44454D0000FF25 /* DDRUMTests.swift */, + A7DA18022AB0C8A700F76337 /* DDUIKitRUMViewsPredicateTests.swift */, + A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */, + 9EE5AD8126205B82001E699E /* DDNSURLSessionDelegateTests.swift */, + 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */, + 61D03BDE273404BB00367DE0 /* RUM */, + F603F1282CAEA4E90088E6B7 /* DDInternalLoggerTests.swift */, ); - path = Utils; + path = DatadogObjc; sourceTree = ""; }; - 61F1A61B2498AD2C00075390 /* SystemFrameworks */ = { + 61133C182423990D00786299 /* Datadog */ = { isa = PBXGroup; children = ( - 61133C202423990D00786299 /* FoundationMocks.swift */, - 61133C1C2423990D00786299 /* UIKitMocks.swift */, - 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */, + 61133C192423990D00786299 /* Mocks */, + D2EFA873286E010100F1FAA6 /* DatadogCore */, + 61133C412423990D00786299 /* DatadogTests.swift */, + 61BBD19624ED50040023E65F /* DatadogConfigurationTests.swift */, + 61133C382423990D00786299 /* LoggerTests.swift */, + 49274903288048AA00ECD49B /* InternalProxyTests.swift */, + 61C5A89524509BF600DA608C /* TracerTests.swift */, + 61133C212423990D00786299 /* Core */, + D24C9C5F29A7CAB0002057CF /* Logs */, + 61C5A89724509C1100DA608C /* Tracing */, + 61E5332D24B75DC7003D6C4E /* RUM */, + 61F2724725C9437C00D54BF8 /* CrashReporting */, + 61216278247D20D500AC5D67 /* FeaturesIntegration */, + 61133C352423990D00786299 /* Utils */, + 61BAD46826415FA2001886CA /* OpenTracing */, + 61D3E0DD277B3D6E008BE766 /* Kronos */, + 6174D6092BFDDDE400EC7469 /* SDKMetrics */, ); - path = SystemFrameworks; + path = Datadog; sourceTree = ""; }; - 9E47010324471027000073A4 /* include */ = { + 61133C192423990D00786299 /* Mocks */ = { isa = PBXGroup; children = ( - 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */, + D20605B4287572270047275C /* DatadogCore */, + D26F59312851E30B0097C455 /* DatadogInternal */, + 61F1A6192498A51700075390 /* CoreMocks.swift */, + 6176991A2A86121B0030022B /* HTTPClientMock.swift */, + D24C9C7029A7D57A002057CF /* DirectoriesMock.swift */, + D24C9C4C29A7B9CA002057CF /* LogsMocks.swift */, + D25CFA9E29C85FA400E3A43D /* TracingFeatureMocks.swift */, + D29A9FCD29DDC470005C54A4 /* RUMFeatureMocks.swift */, + D22743E829DEC9A9001A7EF9 /* RUMDataModelMocks.swift */, + 61F2723E25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift */, + D20605BB28757BFB0047275C /* KronosClockMock.swift */, + 61F1A61B2498AD2C00075390 /* SystemFrameworks */, ); - path = include; + path = Mocks; sourceTree = ""; }; - 9E5D2D4A249137E900763FE4 /* AutoInstrumentation */ = { + 61133C212423990D00786299 /* Core */ = { isa = PBXGroup; children = ( - 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */, - 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */, + 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */, + 61EF78C0257F842000EDCCB3 /* FeatureTests.swift */, + 61345612244756E300E7DA6B /* PerformancePresetTests.swift */, + 618C365D248E858200520CDE /* Utils */, + 61133C272423990D00786299 /* Persistence */, + 6128F5792BA35D2800D35B08 /* DataStore */, + 61F930C02BA1C306005F0EE2 /* TLV */, + 61133C2E2423990D00786299 /* Upload */, + 61E917CD246426E000E6C631 /* Utils */, + 61E945E5286C504B00A946C4 /* DD */, ); - path = AutoInstrumentation; + path = Core; sourceTree = ""; }; - 9E5D2D4B2491382800763FE4 /* AutoInstrumentation */ = { + 61133C272423990D00786299 /* Persistence */ = { isa = PBXGroup; children = ( - 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */, - 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */, - 9E330A8C24ADE1250031408E /* NSURLSessionBridge.h */, - 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */, + 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */, + 3C0D5DDF2A543DAE00446CF9 /* EventGeneratorTests.swift */, + 61133C2A2423990D00786299 /* FilesOrchestratorTests.swift */, + 6136CB492A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift */, + 619E16D42577C11B00B2516B /* Writing */, + 619E16D52577C12100B2516B /* Reading */, + 61133C2B2423990D00786299 /* Files */, ); - path = AutoInstrumentation; + path = Persistence; sourceTree = ""; }; - 9E68FB52244707FD0013A8AA /* _Datadog_Private */ = { + 61133C2B2423990D00786299 /* Files */ = { isa = PBXGroup; children = ( - 9E47010324471027000073A4 /* include */, - 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */, + 61133C2C2423990D00786299 /* FileTests.swift */, + 61133C2D2423990D00786299 /* DirectoryTests.swift */, ); - name = _Datadog_Private; - path = ../Sources/_Datadog_Private; + path = Files; sourceTree = ""; }; - 9EF49F1524476FBD004F2CA0 /* DatadogIntegrationTests */ = { + 61133C2E2423990D00786299 /* Upload */ = { isa = PBXGroup; children = ( - 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */, - 9EF49F1624476FBD004F2CA0 /* Info.plist */, + A7CA21822BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift */, + A7CA217F2BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift */, + 61133C2F2423990D00786299 /* DataUploadWorkerTests.swift */, + 61133C302423990D00786299 /* DataUploadConditionsTests.swift */, + 61133C312423990D00786299 /* DataUploadDelayTests.swift */, + 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */, + 61133C322423990D00786299 /* DataUploaderTests.swift */, + 61133C342423990D00786299 /* URLSessionClientTests.swift */, + 61133C332423990D00786299 /* RequestBuilderTests.swift */, ); - path = DatadogIntegrationTests; + path = Upload; sourceTree = ""; }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 61133B7D242393DE00786299 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 61133B93242393DE00786299 /* Datadog.h in Headers */, - 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */, + 61133C352423990D00786299 /* Utils */ = { + isa = PBXGroup; + children = ( + 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */, + D244B3A2271EDACD003E1B29 /* SwiftUIExtensionsTests.swift */, + D25CFAA129C8644E00E3A43D /* Casting+Tracing.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Utils; + sourceTree = ""; }; - 61133BEB242397DA00786299 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 61133C00242397DA00786299 /* DatadogObjc.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 61133B81242393DE00786299 /* Datadog */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61133B96242393DE00786299 /* Build configuration list for PBXNativeTarget "Datadog" */; - buildPhases = ( - 61133B7D242393DE00786299 /* Headers */, - 61133B7E242393DE00786299 /* Sources */, - 61133B80242393DE00786299 /* Resources */, - 61133C772423A4C300786299 /* ⚙️ Run linter */, - ); - buildRules = ( - ); - dependencies = ( + 61133C3A2423990D00786299 /* Log */ = { + isa = PBXGroup; + children = ( + 61133C3B2423990D00786299 /* LogEventBuilderTests.swift */, + 61133C3C2423990D00786299 /* LogSanitizerTests.swift */, + 615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */, + 615D52C02C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift */, ); - name = Datadog; - productName = Datadog; - productReference = 61133B82242393DE00786299 /* Datadog.framework */; - productType = "com.apple.product-type.framework"; + path = Log; + sourceTree = ""; }; - 61133B8A242393DE00786299 /* DatadogTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61133B99242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogTests" */; - buildPhases = ( - 61133B87242393DE00786299 /* Sources */, - 61133B88242393DE00786299 /* Frameworks */, - 61133B89242393DE00786299 /* Resources */, - 9EA6A53C24489AB100621535 /* ⚙️ Run linter */, - ); - buildRules = ( - ); - dependencies = ( - 61441C5A24619A08003D8BB8 /* PBXTargetDependency */, + 61133C422423990D00786299 /* Matchers */ = { + isa = PBXGroup; + children = ( + 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */, + 61133C432423990D00786299 /* LogMatcher.swift */, + 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */, + 61FF282724B8A31E000B3D9B /* RUMEventMatcher.swift */, + 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */, + 612C13CF2AA772FA0086B5D1 /* SRRequestMatcher.swift */, + 612C13D52AAB35EB0086B5D1 /* SRSegmentMatcher.swift */, ); - name = DatadogTests; - productName = DatadogTests; - productReference = 61133B8B242393DE00786299 /* DatadogTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; + path = Matchers; + sourceTree = ""; }; - 61133BEF242397DA00786299 /* DatadogObjc */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61133C01242397DA00786299 /* Build configuration list for PBXNativeTarget "DatadogObjc" */; - buildPhases = ( - 61133BEB242397DA00786299 /* Headers */, - 61133BEC242397DA00786299 /* Sources */, - 61133BED242397DA00786299 /* Frameworks */, - 61133BEE242397DA00786299 /* Resources */, - 61133C742423993200786299 /* Embed Frameworks */, - ); - buildRules = ( + 61133C442423990D00786299 /* Helpers */ = { + isa = PBXGroup; + children = ( + 61133C472423990D00786299 /* DatadogExtensions.swift */, + 61A763DA252DB2B3005A23F2 /* NSURLSessionBridge.h */, + 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */, + 61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */, + 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */, ); - dependencies = ( - 61133C732423993200786299 /* PBXTargetDependency */, + path = Helpers; + sourceTree = ""; + }; + 61133C6F2423993200786299 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3C1F88222B767CE200821579 /* OpenTelemetryApi.xcframework */, + D2579591298ABCED008A1BE5 /* XCTest.framework */, + D2579593298ABCF5008A1BE5 /* XCTest.framework */, + 61B03ECC274FF00E00EB1AE1 /* SwiftUI.framework */, + 9E5BD8052819742C00CB568E /* SwiftUI.framework */, + 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */, + 9E0542CA25F8EBBE007A3D0B /* Kronos.xcframework */, ); - name = DatadogObjc; - productName = DatadogObjc; - productReference = 61133BF0242397DA00786299 /* DatadogObjc.framework */; - productType = "com.apple.product-type.framework"; + name = Frameworks; + sourceTree = ""; }; - 61441C0124616DE9003D8BB8 /* Example */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example" */; - buildPhases = ( - 61441BFE24616DE9003D8BB8 /* Sources */, - 61441BFF24616DE9003D8BB8 /* Frameworks */, - 61441C0024616DE9003D8BB8 /* Resources */, - 61441C5124619499003D8BB8 /* ⚙️ Embed Framework Dependencies */, + 61216277247D1F2100AC5D67 /* FeaturesIntegration */ = { + isa = PBXGroup; + children = ( + E11625D727B681D200E428C6 /* CITestIntegration.swift */, ); - buildRules = ( + path = FeaturesIntegration; + sourceTree = ""; + }; + 61216278247D20D500AC5D67 /* FeaturesIntegration */ = { + isa = PBXGroup; + children = ( + E143CCAE27D236F600F4018A /* CITestIntegrationTests.swift */, + 61216279247D21FE00AC5D67 /* TracingWithLoggingIntegrationTests.swift */, ); - dependencies = ( - 61441C5024619499003D8BB8 /* PBXTargetDependency */, + path = FeaturesIntegration; + sourceTree = ""; + }; + 61216B812667CFC90089DCD1 /* Helpers */ = { + isa = PBXGroup; + children = ( + 61216B7D2667BC220089DCD1 /* DatadogE2EHelpers.swift */, + 6167C7942666622800D4CF07 /* LoggingE2EHelpers.swift */, + 61216B7826679DD20089DCD1 /* RUME2EHelpers.swift */, ); - name = Example; - packageProductDependencies = ( + path = Helpers; + sourceTree = ""; + }; + 6128F5682BA2237300D35B08 /* DataStore */ = { + isa = PBXGroup; + children = ( + 6128F5692BA2237300D35B08 /* DataStore.swift */, ); - productName = Example; - productReference = 61441C0224616DE9003D8BB8 /* Example.app */; - productType = "com.apple.product-type.application"; + path = DataStore; + sourceTree = ""; }; - 61441C2924616F1D003D8BB8 /* DatadogIntegrationTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61441C3124616F1D003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */; - buildPhases = ( - 61441C2624616F1D003D8BB8 /* Sources */, - 61441C2724616F1D003D8BB8 /* Frameworks */, - 61441C2824616F1D003D8BB8 /* Resources */, + 6128F56C2BA223A100D35B08 /* DataStore */ = { + isa = PBXGroup; + children = ( + 6128F5702BA223D100D35B08 /* DataStore+TLV.swift */, + 6128F56D2BA223A100D35B08 /* FeatureDataStore.swift */, + 6128F5732BA3280300D35B08 /* DataStoreFileReader.swift */, + 6128F5762BA32DE500D35B08 /* DataStoreFileWriter.swift */, ); - buildRules = ( + path = DataStore; + sourceTree = ""; + }; + 6128F5792BA35D2800D35B08 /* DataStore */ = { + isa = PBXGroup; + children = ( + 6128F57D2BA8A3A000D35B08 /* DataStore+TLVTests.swift */, + 6128F57A2BA35D6200D35B08 /* FeatureDataStoreTests.swift */, + 6128F5832BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift */, + 6128F5892BA9860B00D35B08 /* DataStoreFileReaderTests.swift */, ); - dependencies = ( - 61441C3024616F1D003D8BB8 /* PBXTargetDependency */, + path = DataStore; + sourceTree = ""; + }; + 6132BF4024A38D0600D7BD17 /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */, + 615A4A8A24A3568900233986 /* OTSpan+objc.swift */, + 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */, ); - name = DatadogIntegrationTests; - packageProductDependencies = ( - 61441C43246174CE003D8BB8 /* HTTPServerMock */, + path = OpenTracing; + sourceTree = ""; + }; + 6132BF4524A498B400D7BD17 /* Tracing */ = { + isa = PBXGroup; + children = ( + 615A4A8224A3431600233986 /* Trace+objc.swift */, + 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */, + 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */, + 6132BF4A24A49C7200D7BD17 /* Propagation */, + 6132BF4F24A49F6400D7BD17 /* Utils */, ); - productName = DatadogIntegrationTests; - productReference = 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; + path = Tracing; + sourceTree = ""; }; - 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */; - buildPhases = ( - 61441C6424619FE4003D8BB8 /* Sources */, - 61441C6524619FE4003D8BB8 /* Frameworks */, - 61441C6624619FE4003D8BB8 /* Resources */, + 6132BF4A24A49C7200D7BD17 /* Propagation */ = { + isa = PBXGroup; + children = ( + 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */, + 3CA852612BF2147600B52CBA /* TraceContextInjection+objc.swift */, + 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, + A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */, + A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */, ); - buildRules = ( + path = Propagation; + sourceTree = ""; + }; + 6132BF4F24A49F6400D7BD17 /* Utils */ = { + isa = PBXGroup; + children = ( + 6132BF5024A49F7400D7BD17 /* Casting.swift */, ); - dependencies = ( - 61441C7524619FED003D8BB8 /* PBXTargetDependency */, + path = Utils; + sourceTree = ""; + }; + 613E79412577C08900DFCC17 /* Writing */ = { + isa = PBXGroup; + children = ( + 61133BA72423979B00786299 /* FileWriter.swift */, + D2303A09298D5412001A1FA3 /* AsyncWriter.swift */, ); - name = DatadogBenchmarkTests; - productName = DatadogBenchmarkTests; - productReference = 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; + path = Writing; + sourceTree = ""; }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 61133B79242393DE00786299 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1140; - LastUpgradeCheck = 1130; - ORGANIZATIONNAME = Datadog; - TargetAttributes = { - 61133B81242393DE00786299 = { - CreatedOnToolsVersion = 11.3.1; - }; - 61133B8A242393DE00786299 = { - CreatedOnToolsVersion = 11.3.1; - TestTargetID = 61441C0124616DE9003D8BB8; - }; - 61133BEF242397DA00786299 = { - CreatedOnToolsVersion = 11.3.1; - }; - 61441C0124616DE9003D8BB8 = { - CreatedOnToolsVersion = 11.4; - }; - 61441C2924616F1D003D8BB8 = { - CreatedOnToolsVersion = 11.4; - TestTargetID = 61441C0124616DE9003D8BB8; - }; - 61441C6724619FE4003D8BB8 = { - CreatedOnToolsVersion = 11.4; - TestTargetID = 61441C0124616DE9003D8BB8; - }; - }; - }; - buildConfigurationList = 61133B7C242393DE00786299 /* Build configuration list for PBXProject "Datadog" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, + 613E79422577C09B00DFCC17 /* Reading */ = { + isa = PBXGroup; + children = ( + 613E792E2577B0F900DFCC17 /* Reader.swift */, + 613E793A2577B6EE00DFCC17 /* DataReader.swift */, + 61133BAD2423979B00786299 /* FileReader.swift */, ); - mainGroup = 61133B78242393DE00786299; - productRefGroup = 61133B83242393DE00786299 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 61133B81242393DE00786299 /* Datadog */, - 61133BEF242397DA00786299 /* DatadogObjc */, - 61133B8A242393DE00786299 /* DatadogTests */, - 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */, - 61441C2924616F1D003D8BB8 /* DatadogIntegrationTests */, - 61441C0124616DE9003D8BB8 /* Example */, + path = Reading; + sourceTree = ""; + }; + 613E81EE25A73FB90084B751 /* Scrubbing */ = { + isa = PBXGroup; + children = ( + 613E81EF25A740140084B751 /* RUMEventsMapper.swift */, ); + path = Scrubbing; + sourceTree = ""; }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 61133B80242393DE00786299 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 613E81F525A743470084B751 /* Scrubbing */ = { + isa = PBXGroup; + children = ( + 613E81F625A743600084B751 /* RUMEventsMapperTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Scrubbing; + sourceTree = ""; }; - 61133B89242393DE00786299 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 613F23EF252B1287006CD2D7 /* Resources */ = { + isa = PBXGroup; + children = ( + D2BCB12129D34A5F00737A9A /* URLSessionRUMResourcesHandlerTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Resources; + sourceTree = ""; }; - 61133BEE242397DA00786299 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 6141014C251A577D00E3C2D9 /* Actions */ = { + isa = PBXGroup; + children = ( + 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Actions; + sourceTree = ""; }; - 61441C0024616DE9003D8BB8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */, - 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */, - 61441C0C24616DE9003D8BB8 /* Main.storyboard in Resources */, + 6141014D251A578D00E3C2D9 /* Actions */ = { + isa = PBXGroup; + children = ( + 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */, + D29D5A4A273BF81500A687C1 /* UIKit */, + D29D5A4B273BF82200A687C1 /* SwiftUI */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Actions; + sourceTree = ""; }; - 61441C2824616F1D003D8BB8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 61411B0E24EC15940012EAB2 /* Utils */ = { + isa = PBXGroup; + children = ( + 611529AD25E3E429004F740E /* ValuePublisherTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Utils; + sourceTree = ""; }; - 61441C6624619FE4003D8BB8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 6141CE652806B3F200EBB879 /* Utils */ = { + isa = PBXGroup; + children = ( + 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */, + 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */, + D253EE982B98B3690010B589 /* ViewCacheTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Utils; + sourceTree = ""; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 61133C772423A4C300786299 /* ⚙️ Run linter */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( + 61441C0324616DE9003D8BB8 /* Example */ = { + isa = PBXGroup; + children = ( + 61441C0424616DE9003D8BB8 /* ExampleAppDelegate.swift */, + 614CADD62510BAC000B93D2D /* Environment.swift */, + 61441C9A2461A64F003D8BB8 /* Debugging */, + 61441C8F2461A648003D8BB8 /* Utils */, + 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */, + 61441C0D24616DEC003D8BB8 /* Assets.xcassets */, + D240688427CFA64A00C04F44 /* LaunchScreen.storyboard */, ); - inputFileListPaths = ( + path = Example; + sourceTree = ""; + }; + 61441C8F2461A648003D8BB8 /* Utils */ = { + isa = PBXGroup; + children = ( + 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */, + 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */, + D2F44FC1299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift */, + 3C62C3602C3E852F00C7E336 /* MultiSelector.swift */, ); - inputPaths = ( + path = Utils; + sourceTree = ""; + }; + 61441C9A2461A64F003D8BB8 /* Debugging */ = { + isa = PBXGroup; + children = ( + 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */, + 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */, + 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */, + 617699202A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift */, + 61E5333724B84EE2003D6C4E /* DebugRUMViewController.swift */, + 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */, + 618236882710560900125326 /* DebugWebviewViewController.swift */, + 61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */, + 6111543425C993AC007C84C9 /* CrashReporting */, + 61020C272757AD63005EEAEA /* BackgroundEvents */, + 61776D4C273E6D8100F93802 /* Helpers */, ); - name = "⚙️ Run linter"; - outputFileListPaths = ( + path = Debugging; + sourceTree = ""; + }; + 61441C9E2461AF4D003D8BB8 /* Example */ = { + isa = PBXGroup; + children = ( + 61441C1224616DEC003D8BB8 /* Info.plist */, + E1B082CB25641DF9002DB9D2 /* Example.xcconfig */, + 6167ACBD251A0B410012B4D0 /* Example-Bridging-Header.h */, ); - outputPaths = ( + path = Example; + sourceTree = ""; + }; + 61494B7827F3522C0082BBCC /* Utils */ = { + isa = PBXGroup; + children = ( + 61FF9A4425AC5DEA001058CC /* ViewIdentifier.swift */, + 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */, + D253EE952B988CA90010B589 /* ViewCache.swift */, ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; - showEnvVarsInLog = 0; + path = Utils; + sourceTree = ""; }; - 9EA6A53C24489AB100621535 /* ⚙️ Run linter */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( + 615519242461BCE7002A85CF /* xcconfigs */ = { + isa = PBXGroup; + children = ( + 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */, + 61569894256D0E9A00C6AADA /* Base.xcconfig */, + 6185EB0F25FA94A700B43E2E /* Base.local.xcconfig */, + 615519252461BCE7002A85CF /* Datadog.xcconfig */, + 615519262461BCE7002A85CF /* Datadog.local.xcconfig */, + 61378BB22555337900F28837 /* DatadogSDKTesting.local.xcconfig */, ); - inputFileListPaths = ( + name = xcconfigs; + path = ../../xcconfigs; + sourceTree = ""; + }; + 6156CB8A24DDA186008CB2B2 /* RUMContext */ = { + isa = PBXGroup; + children = ( + 61C3E63824BF19B4008053F2 /* RUMContext.swift */, + 6156CB8D24DDA1B5008CB2B2 /* RUMContextProvider.swift */, ); - inputPaths = ( + path = RUMContext; + sourceTree = ""; + }; + 6157FA5C252767B3009A8A3B /* Resources */ = { + isa = PBXGroup; + children = ( + D2BCB11E29D30AF000737A9A /* URLSessionRUMResourcesHandler.swift */, ); - name = "⚙️ Run linter"; - outputFileListPaths = ( + path = Resources; + sourceTree = ""; + }; + 615950EC291C057D00470E0C /* Integrations */ = { + isa = PBXGroup; + children = ( + D2CBC26D294395A300134409 /* RUMContextAttributes.swift */, + 615950ED291C058F00470E0C /* SessionReplayDependency.swift */, + D2CBC26A294383F200134409 /* WebViewEventReceiver.swift */, + D236BE2729520FED00676E67 /* CrashReportReceiver.swift */, + D215ED6A29D2E1080046B721 /* ErrorMessageReceiver.swift */, + D214DAA729E54CB4004D0AE8 /* TelemetryReceiver.swift */, + 61DCC84D2C071DCD00CB59E5 /* TelemetryInterceptor.swift */, + ); + path = Integrations; + sourceTree = ""; + }; + 615CC40A2694A55D0005F08C /* Utils */ = { + isa = PBXGroup; + children = ( + 615CC40B2694A56D0005F08C /* SwiftExtensions.swift */, ); - outputPaths = ( + path = Utils; + sourceTree = ""; + }; + 615CC40E2694A63A0005F08C /* Utils */ = { + isa = PBXGroup; + children = ( + 615CC40F2694A64D0005F08C /* SwiftExtensionTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; - showEnvVarsInLog = 0; + path = Utils; + sourceTree = ""; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 61133B7E242393DE00786299 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */, - 61E909ED24A24DD3005EA2DE /* OTSpan.swift in Sources */, - 61133BDE2423979B00786299 /* CompilationConditions.swift in Sources */, - 61E909F324A24DD3005EA2DE /* OTSpanContext.swift in Sources */, - 61E909F024A24DD3005EA2DE /* OTTracer.swift in Sources */, - 61E909F124A24DD3005EA2DE /* OTReference.swift in Sources */, - 61216276247D1CD700AC5D67 /* LoggingForTracingAdapter.swift in Sources */, - 61E909EF24A24DD3005EA2DE /* OTGlobal.swift in Sources */, - 61133BDD2423979B00786299 /* InternalLoggers.swift in Sources */, - 61133BDC2423979B00786299 /* Logger.swift in Sources */, - 61133BD02423979B00786299 /* DateProvider.swift in Sources */, - 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */, - 61133BCF2423979B00786299 /* FileWriter.swift in Sources */, - 61E909F224A24DD3005EA2DE /* OTConstants.swift in Sources */, - 61133BCC2423979B00786299 /* MobileDevice.swift in Sources */, - 61C5A8A724509FAA00DA608C /* SpanBuilder.swift in Sources */, - 61AD4E3824531500006E34EA /* DataFormat.swift in Sources */, - 9E58E8E124615C75008E5063 /* JSONEncoder.swift in Sources */, - 61133BCA2423979B00786299 /* EncodableValue.swift in Sources */, - 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, - 9ED583A32498C222004CFF2A /* TracingAutoInstrumentation.swift in Sources */, - 617CEB392456BC3A00AD4669 /* TracingUUID.swift in Sources */, - 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */, - 61C5A88824509A0C00DA608C /* Warnings.swift in Sources */, - 61C5A88924509A0C00DA608C /* DDSpanContext.swift in Sources */, - 61133BE62423979B00786299 /* LogSanitizer.swift in Sources */, - 61133BDF2423979B00786299 /* SwiftExtensions.swift in Sources */, - 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */, - 61133BEA2423979B00786299 /* LogConsoleOutput.swift in Sources */, - 61C5A8A624509FAA00DA608C /* SpanEncoder.swift in Sources */, - 61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */, - 61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */, - 61133BE32423979B00786299 /* UserInfo.swift in Sources */, - 61133BE02423979B00786299 /* Datadog.swift in Sources */, - 61133BCB2423979B00786299 /* CarrierInfoProvider.swift in Sources */, - 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */, - 61133BD62423979B00786299 /* DataUploader.swift in Sources */, - 61C5A88724509A0C00DA608C /* Casting.swift in Sources */, - 61133BE52423979B00786299 /* LogBuilder.swift in Sources */, - 61133BD42423979B00786299 /* FileReader.swift in Sources */, - 61C5A88A24509A0C00DA608C /* SpanFileOutput.swift in Sources */, - 61133BD32423979B00786299 /* File.swift in Sources */, - 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */, - 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */, - 61133BDA2423979B00786299 /* HTTPHeaders.swift in Sources */, - 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, - 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, - 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, - 61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */, - 61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */, - 61133BE42423979B00786299 /* LogEncoder.swift in Sources */, - 61C5A88424509A0C00DA608C /* DDSpan.swift in Sources */, - 61133BD82423979B00786299 /* HTTPClient.swift in Sources */, - 61133BDB2423979B00786299 /* DatadogConfiguration.swift in Sources */, - 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */, - 61133BCE2423979B00786299 /* BatteryStatusProvider.swift in Sources */, - 61133BD52423979B00786299 /* DataUploadConditions.swift in Sources */, - 612983CD2449E62E00D4424B /* LoggingFeature.swift in Sources */, - 9EB47B92247443FA004F90BE /* URLSessionSwizzler.swift in Sources */, - 61133BE92423979B00786299 /* LogOutput.swift in Sources */, - 61C5A88524509A0C00DA608C /* DDNoOps.swift in Sources */, - 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */, - 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, - 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */, - 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, - 61133BD22423979B00786299 /* Directory.swift in Sources */, + 616124AF25CAC26B009901BE /* CrashContext */ = { + isa = PBXGroup; + children = ( + 6161249D25CAB340009901BE /* CrashContext.swift */, + 616124A625CAC268009901BE /* CrashContextProvider.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = CrashContext; + sourceTree = ""; }; - 61133B87242393DE00786299 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61C5A8A024509C1100DA608C /* Casting.swift in Sources */, - 61133C662423990D00786299 /* LogSanitizerTests.swift in Sources */, - 9E330A8E24ADE1250031408E /* NSURLSessionBridge.m in Sources */, - 9E493E1C249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift in Sources */, - 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */, - 61C36470243B5C8300C4D4E6 /* ServerMock.swift in Sources */, - 61133C5D2423990D00786299 /* DataUploadConditionsTests.swift in Sources */, - 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */, - 61133C5A2423990D00786299 /* FileTests.swift in Sources */, - 61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */, - 61133C6B2423990D00786299 /* LogMatcher.swift in Sources */, - 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */, - 61133C582423990D00786299 /* FileWriterTests.swift in Sources */, - 9E544A4D24752A8900E83072 /* URLSessionSwizzlerTests.swift in Sources */, - 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */, - 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */, - 61133C672423990D00786299 /* LogConsoleOutputTests.swift in Sources */, - 61FB222D244A21ED00902D19 /* LoggingFeatureMocks.swift in Sources */, - 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, - 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */, - 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */, - 615A4A8724A3452800233986 /* DDTracerConfigurationTests.swift in Sources */, - 61F1A621249A45E400075390 /* DDSpanContextTests.swift in Sources */, - 61E917CF2464270500E6C631 /* EncodableValueTests.swift in Sources */, - 61133C542423990D00786299 /* NetworkConnectionInfoProviderTests.swift in Sources */, - 61B558CF2469561C001460D3 /* LoggerBuilderTests.swift in Sources */, - 61133C4A2423990D00786299 /* DDConfigurationTests.swift in Sources */, - 61C5A89F24509C1100DA608C /* UUID.swift in Sources */, - 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */, - 61133C602423990D00786299 /* HTTPHeadersTests.swift in Sources */, - 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, - 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */, - 61133C612423990D00786299 /* HTTPClientTests.swift in Sources */, - 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, - 61133C5E2423990D00786299 /* LogsUploadDelayTests.swift in Sources */, - 61133C5C2423990D00786299 /* DataUploadWorkerTests.swift in Sources */, - 61E909F624A32D1C005EA2DE /* OTGlobalTests.swift in Sources */, - 9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */, - 61133C692423990D00786299 /* LogFileOutputTests.swift in Sources */, - 61133C682423990D00786299 /* LogUtilityOutputsTests.swift in Sources */, - 61133C6E2423990D00786299 /* DatadogExtensions.swift in Sources */, - 61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */, - 61133C592423990D00786299 /* FilesOrchestratorTests.swift in Sources */, - 61B558D42469CDD8001460D3 /* TracingUUIDGeneratorTests.swift in Sources */, - 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */, - 61FB2230244E1BE900902D19 /* LoggingFeatureTests.swift in Sources */, - 61133C6D2423990D00786299 /* TestsDirectory.swift in Sources */, - 61133C6C2423990D00786299 /* SwiftExtensions.swift in Sources */, - 61133C492423990D00786299 /* DDLoggerBuilderTests.swift in Sources */, - 61133C4B2423990D00786299 /* DDLoggerTests.swift in Sources */, - 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */, - 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */, - 61E45BD22450F65B00F2C652 /* SpanBuilderTests.swift in Sources */, - 61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */, - 61133C482423990D00786299 /* DDDatadogTests.swift in Sources */, - 61133C522423990D00786299 /* FoundationMocks.swift in Sources */, - 61133C5B2423990D00786299 /* DirectoryTests.swift in Sources */, - 61133C562423990D00786299 /* CarrierInfoProviderTests.swift in Sources */, - 61C5A89E24509C1100DA608C /* WarningsTests.swift in Sources */, - 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */, - 61133C652423990D00786299 /* LogBuilderTests.swift in Sources */, - 61F8CC092469295500FE2908 /* DatadogConfigurationTests.swift in Sources */, - 61F1A623249B811200075390 /* Encoding.swift in Sources */, - 61133C642423990D00786299 /* LoggerTests.swift in Sources */, - 61E45BE5245196EA00F2C652 /* SpanFileOutputTests.swift in Sources */, - 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */, - 61133C4D2423990D00786299 /* CoreTelephonyMocks.swift in Sources */, - 61133C552423990D00786299 /* BatteryStatusProviderTests.swift in Sources */, - 6121627C247D220500AC5D67 /* LoggingForTracingAdapterTests.swift in Sources */, - 61133C532423990D00786299 /* MobileDeviceTests.swift in Sources */, - 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */, + 6167E6D12B7F8B1300C3CA2D /* AppHangs */ = { + isa = PBXGroup; + children = ( + 61F930CA2BA213AC005F0EE2 /* AppHang.swift */, + 6167E6D22B7F8B3300C3CA2D /* AppHangsMonitor.swift */, + 6194B92F2BB451C100179430 /* NonFatalAppHangsHandler.swift */, + 6194B9322BB451DB00179430 /* FatalAppHangsHandler.swift */, + 6167E6D52B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift */, + 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */, + ); + path = AppHangs; + sourceTree = ""; + }; + 6167E6D82B80047900C3CA2D /* AppHangs */ = { + isa = PBXGroup; + children = ( + 615B0F8A2BB33C2800E9ED6C /* AppHangsMonitorTests.swift */, + 6167E6D92B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = AppHangs; + sourceTree = ""; }; - 61133BEC242397DA00786299 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61133C0F2423983800786299 /* AnyEncodable.swift in Sources */, - 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */, - 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */, - 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */, - 615A4A8524A3445700233986 /* TracerConfiguration+objc.swift in Sources */, - 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */, - 61133C102423983800786299 /* Logger+objc.swift in Sources */, - 615A4A8324A3431600233986 /* Tracer+objc.swift in Sources */, - 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, - 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */, - 6132BF4E24A49D5400D7BD17 /* OTNoop.swift in Sources */, - 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */, - 6132BF4424A3AAD700D7BD17 /* OTGlobal+objc.swift in Sources */, - 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */, - 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */, + 6167E6DF2B81203A00C3CA2D /* Models */ = { + isa = PBXGroup; + children = ( + D2EA0F442C0E1A8700CB20F8 /* SessionReplay */, + 619F5CEB2BF5089B004BFE70 /* RUM */, + D25C834D2B88A261008E73B1 /* WebViewTracking */, + 6167E6E02B81204B00C3CA2D /* CrashReporting */, ); - runOnlyForDeploymentPostprocessing = 0; + path = Models; + sourceTree = ""; }; - 61441BFE24616DE9003D8BB8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, - 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */, - 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */, - 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */, - 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */, - 61441C9D2461A796003D8BB8 /* AppConfig.swift in Sources */, - 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */, - 61441C992461A649003D8BB8 /* DebugLoggingViewController.swift in Sources */, - 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */, + 6167E6E02B81204B00C3CA2D /* CrashReporting */ = { + isa = PBXGroup; + children = ( + 6167E6E12B81207200C3CA2D /* DDCrashReport.swift */, + 6167E6E72B8122E900C3CA2D /* BacktraceReport.swift */, + 6167E6F52B81E94C00C3CA2D /* DDThread.swift */, + 6167E6F82B81E95900C3CA2D /* BinaryImage.swift */, + 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = CrashReporting; + sourceTree = ""; }; - 61441C2624616F1D003D8BB8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */, - 61B9ED212462089600C0DCFF /* TracingIntegrationTests.swift in Sources */, - 61B9ED1F2461E57700C0DCFF /* UITestsHelpers.swift in Sources */, - 61441C4124617013003D8BB8 /* LoggingIntegrationTests.swift in Sources */, - 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */, - 61441C4924618052003D8BB8 /* JSONDataMatcher.swift in Sources */, - 61441C4A24618052003D8BB8 /* LogMatcher.swift in Sources */, + 6167E6FB2B81EBD100C3CA2D /* BacktraceReporting */ = { + isa = PBXGroup; + children = ( + 6167E6FC2B81EC0400C3CA2D /* BacktraceReporter.swift */, + 6167E6FF2B81EF7500C3CA2D /* BacktraceReportingFeature.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = BacktraceReporting; + sourceTree = ""; }; - 61441C6424619FE4003D8BB8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */, - 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */, - 61441C7C2461A244003D8BB8 /* TestsDirectory.swift in Sources */, + 6167E7162B837F4200C3CA2D /* FeatureModels */ = { + isa = PBXGroup; + children = ( + 6167E7172B837F6300C3CA2D /* CrashReporting */, + D2C9A2852C0F4660007526F5 /* SessionReplayConfigurationMocks.swift */, ); - runOnlyForDeploymentPostprocessing = 0; + path = FeatureModels; + sourceTree = ""; }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 61133C732423993200786299 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61133B81242393DE00786299 /* Datadog */; - targetProxy = 61133C722423993200786299 /* PBXContainerItemProxy */; + 6167E7172B837F6300C3CA2D /* CrashReporting */ = { + isa = PBXGroup; + children = ( + 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */, + 6167E7182B837F7A00C3CA2D /* BacktraceReportMocks.swift */, + 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */, + 6167E71D2B837FB200C3CA2D /* DDThreadMocks.swift */, + ); + path = CrashReporting; + sourceTree = ""; }; - 61441C3024616F1D003D8BB8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61441C0124616DE9003D8BB8 /* Example */; - targetProxy = 61441C2F24616F1D003D8BB8 /* PBXContainerItemProxy */; + 616CCE11250A181C009FED46 /* Instrumentation */ = { + isa = PBXGroup; + children = ( + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */, + 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */, + 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */, + 61F3CDA1251118DD00C816E5 /* Views */, + 6141014D251A578D00E3C2D9 /* Actions */, + 6157FA5C252767B3009A8A3B /* Resources */, + 9E06058F26EF904200F5F935 /* LongTasks */, + 6167E6D12B7F8B1300C3CA2D /* AppHangs */, + 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */, + ); + path = Instrumentation; + sourceTree = ""; }; - 61441C5024619499003D8BB8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61133B81242393DE00786299 /* Datadog */; - targetProxy = 61441C4F24619499003D8BB8 /* PBXContainerItemProxy */; + 6170DC0525C184FA003AED5C /* DatadogCrashReporting */ = { + isa = PBXGroup; + children = ( + 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */, + 61B7885625C180CB002675B5 /* DatadogCrashReporting.h */, + 61B7885725C180CB002675B5 /* Info.plist */, + ); + path = DatadogCrashReporting; + sourceTree = ""; }; - 61441C5A24619A08003D8BB8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61441C0124616DE9003D8BB8 /* Example */; - targetProxy = 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */; + 6170DC0625C184FA003AED5C /* DatadogCrashReportingTests */ = { + isa = PBXGroup; + children = ( + 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */, + 61B7886325C180CB002675B5 /* Info.plist */, + ); + path = DatadogCrashReportingTests; + sourceTree = ""; }; - 61441C7524619FED003D8BB8 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61441C0124616DE9003D8BB8 /* Example */; - targetProxy = 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */; + 6170DC1325C1864B003AED5C /* DatadogCrashReporting */ = { + isa = PBXGroup; + children = ( + D2A7A9012BA1C4B100F46845 /* PrivacyInfo.xcprivacy */, + 6161247825CA9CA6009901BE /* CrashReporting.swift */, + 61DE333525C8278A008E3EC2 /* CrashReportingPlugin.swift */, + D293302B2A137DAD0029C9EA /* CrashReportingFeature.swift */, + 61F2727325C9509D00D54BF8 /* ThirdPartyCrashReporter.swift */, + 616124AF25CAC26B009901BE /* CrashContext */, + D24CA32F28B3BF0C007B26BF /* Integrations */, + 61F2727225C9507C00D54BF8 /* PLCrashReporterIntegration */, + ); + name = DatadogCrashReporting; + path = ../DatadogCrashReporting/Sources; + sourceTree = ""; }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 61441C0A24616DE9003D8BB8 /* Main.storyboard */ = { - isa = PBXVariantGroup; + 6170DC1425C18663003AED5C /* DatadogCrashReportingTests */ = { + isa = PBXGroup; children = ( - 61441C0B24616DE9003D8BB8 /* Base */, + 61F2729A25C95EB200D54BF8 /* Mocks.swift */, + 61B7886125C180CB002675B5 /* CrashReportingPluginTests.swift */, + 61FCBFE525DBBE7D00CCF864 /* PLCrashReporterIntegration */, ); - name = Main.storyboard; + name = DatadogCrashReportingTests; + path = ../DatadogCrashReporting/Tests; sourceTree = ""; }; - 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; + 6174D6072BFCCDA400EC7469 /* ObjC */ = { + isa = PBXGroup; children = ( - 61441C1024616DEC003D8BB8 /* Base */, + 6174D6032BFB9AB600EC7469 /* WebViewTracking+objc.swift */, ); - name = LaunchScreen.storyboard; + path = ObjC; sourceTree = ""; }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 61133B94242393DE00786299 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - 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; + 6174D6082BFDDD1E00EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 614396712A67D74F00197326 /* BatchMetrics.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D6092BFDDDE400EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6134CDB02A691E850061CCD9 /* BatchMetricsTests.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D60A2BFDDE1900EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6174D60B2BFDDEDF00EC7469 /* SDKMetricFields.swift */, + A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D60E2BFDEA1F00EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6174D60F2BFDEA4600EC7469 /* SessionEndedMetric.swift */, + 6174D61F2C009C6300EC7469 /* SessionEndedMetricController.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 6174D6182BFE447600EC7469 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 6174D6192BFE449300EC7469 /* SessionEndedMetricTests.swift */, + 61DCC8462C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 617699162A8608C20030022B /* Context */ = { + isa = PBXGroup; + children = ( + 614B78EC296D7B63009C6B92 /* LowPowerModePublisherTests.swift */, + D2EFA874286E011900F1FAA6 /* DatadogContextProviderTests.swift */, + D29294E2291D652900F8EFF9 /* ApplicationVersionPublisherTests.swift */, + D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */, + D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */, + D2FB1256292E0F0B005B13F8 /* TrackingConsentPublisherTests.swift */, + D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */, + D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */, + D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */, + D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */, + D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */, + D234613028B7712F00055D4C /* FeatureContextTests.swift */, + ); + path = Context; + sourceTree = ""; + }; + 61776D4C273E6D8100F93802 /* Helpers */ = { + isa = PBXGroup; + children = ( + 61776D4D273E6D9F00F93802 /* SwiftUI.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 6179DB542B60229D00E9E04E /* CrashReporting */ = { + isa = PBXGroup; + children = ( + 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */, + 6167E7052B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift */, + ); + path = CrashReporting; + sourceTree = ""; + }; + 617B953B24BF4D7300E6F443 /* RUMMonitor */ = { + isa = PBXGroup; + children = ( + 617B953E24BF4D9D00E6F443 /* Scopes */, + 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */, + 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */, + 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */, + 61CE2E5E2BF2177100EC7D42 /* Monitor+GlobalAttributesTests.swift */, + ); + path = RUMMonitor; + sourceTree = ""; + }; + 617B953E24BF4D9D00E6F443 /* Scopes */ = { + isa = PBXGroup; + children = ( + 6141CE652806B3F200EBB879 /* Utils */, + 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */, + 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */, + 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */, + 61494CB424C864680082C633 /* RUMResourceScopeTests.swift */, + 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */, + 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */, + ); + path = Scopes; + sourceTree = ""; + }; + 618353BA2A6946F40085F84A /* Internal */ = { + isa = PBXGroup; + children = ( + 618353BB2A69470A0085F84A /* CoreMetricsIntegrationTests.swift */, + ); + path = Internal; + sourceTree = ""; + }; + 6184751626EFD01600C7C9C5 /* TestsObserver */ = { + isa = PBXGroup; + children = ( + 6184751426EFCF1300C7C9C5 /* DatadogTestsObserver.swift */, + 6184751726EFD03400C7C9C5 /* DatadogTestsObserverLoader.m */, + ); + path = TestsObserver; + sourceTree = ""; + }; + 618715FA24DC5EE700FC0F69 /* DataModels */ = { + isa = PBXGroup; + children = ( + 618715FB24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift */, + ); + path = DataModels; + sourceTree = ""; + }; + 6187A53726FCBDD00015D94A /* Tracing */ = { + isa = PBXGroup; + children = ( + 6147E3B2270486920092BC9F /* TraceConfigurationE2ETests.swift */, + 6187A53826FCBE240015D94A /* TracerE2ETests.swift */, + 6185F4AD26FE1956001A7641 /* SpanE2ETests.swift */, + ); + path = Tracing; + sourceTree = ""; + }; + 6188900D2AC58B5D00D0B966 /* MessageReceiverMocks */ = { + isa = PBXGroup; + children = ( + D21C26D628A647DB005DD405 /* FeatureMessageReceiverMock.swift */, + 6188900E2AC58B8C00D0B966 /* TelemetryReceiverMock.swift */, + ); + path = MessageReceiverMocks; + sourceTree = ""; + }; + 618C365D248E858200520CDE /* Utils */ = { + isa = PBXGroup; + children = ( + 618C365E248E85B400520CDE /* DateFormattingTests.swift */, + 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */, + 61363D9E24D99BAA0084CD6F /* DDErrorTests.swift */, + 61DA8CB1286215DE0074A606 /* CryptographyTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 618D9DE5263AD77600A3FAD2 /* Scrubbing */ = { + isa = PBXGroup; + children = ( + 618D9DE6263AD78900A3FAD2 /* SpanEventMapper.swift */, + ); + path = Scrubbing; + sourceTree = ""; + }; + 618DCFD524C7264100589570 /* UUIDs */ = { + isa = PBXGroup; + children = ( + 618DCFD824C7269500589570 /* RUMUUIDGenerator.swift */, + 618DCFD624C7265300589570 /* RUMUUID.swift */, + ); + path = UUIDs; + sourceTree = ""; + }; + 618F9841265BC486009959F8 /* E2EInstrumentationTests */ = { + isa = PBXGroup; + children = ( + 618F9842265BC486009959F8 /* E2EInstrumentationTests.swift */, + ); + path = E2EInstrumentationTests; + sourceTree = ""; + }; + 618F984B265BC4C0009959F8 /* E2EInstrumentationTests */ = { + isa = PBXGroup; + children = ( + 618F9844265BC486009959F8 /* Info.plist */, + 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */, + ); + path = E2EInstrumentationTests; + sourceTree = ""; + }; + 6199362C265BA959009D7EA8 /* E2E */ = { + isa = PBXGroup; + children = ( + 618F984D265BC905009959F8 /* E2EConfig.swift */, + 6199362D265BA959009D7EA8 /* E2EAppDelegate.swift */, + 61993636265BA95A009D7EA8 /* Assets.xcassets */, + 61993638265BA95A009D7EA8 /* LaunchScreen.storyboard */, + ); + path = E2E; + sourceTree = ""; + }; + 61993640265BAC34009D7EA8 /* E2E */ = { + isa = PBXGroup; + children = ( + 6199363B265BA95A009D7EA8 /* Info.plist */, + 61993641265BAD2D009D7EA8 /* E2E.xcconfig */, + ); + path = E2E; + sourceTree = ""; + }; + 61993666265BBEDC009D7EA8 /* E2ETests */ = { + isa = PBXGroup; + children = ( + 61D3E0E8277E0C16008BE766 /* NTP */, + 9E64849B27031050007CCD7B /* RUM */, + 61B3BD502661224800A9BEF0 /* Logging */, + 6187A53726FCBDD00015D94A /* Tracing */, + 61993667265BBEDC009D7EA8 /* E2ETests.swift */, + 6167C79226665D6900D4CF07 /* E2EUtils.swift */, + 61216B812667CFC90089DCD1 /* Helpers */, + ); + path = E2ETests; + sourceTree = ""; + }; + 61993670265BBF19009D7EA8 /* E2ETests */ = { + isa = PBXGroup; + children = ( + 61993669265BBEDC009D7EA8 /* Info.plist */, + 61993672265BC029009D7EA8 /* E2ETests.xcconfig */, + ); + path = E2ETests; + sourceTree = ""; + }; + 619E16D42577C11B00B2516B /* Writing */ = { + isa = PBXGroup; + children = ( + 61133C292423990D00786299 /* FileWriterTests.swift */, + ); + path = Writing; + sourceTree = ""; + }; + 619E16D52577C12100B2516B /* Reading */ = { + isa = PBXGroup; + children = ( + 61133C282423990D00786299 /* FileReaderTests.swift */, + ); + path = Reading; + sourceTree = ""; + }; + 619F5CEB2BF5089B004BFE70 /* RUM */ = { + isa = PBXGroup; + children = ( + 619F5CEA2BF5089B004BFE70 /* GlobalRUMAttributes.swift */, + ); + path = RUM; + sourceTree = ""; + }; + 61AE74112AD6EE7E008DB9BB /* Matchers */ = { + isa = PBXGroup; + children = ( + 612C13D22AAA20660086B5D1 /* JSONObjectMatcher.swift */, + ); + path = Matchers; + sourceTree = ""; + }; + 61B22E5824F3E6A700DC26D2 /* Debugging */ = { + isa = PBXGroup; + children = ( + 61B22E5924F3E6B700DC26D2 /* RUMDebugging.swift */, + ); + path = Debugging; + sourceTree = ""; + }; + 61B3BD502661224800A9BEF0 /* Logging */ = { + isa = PBXGroup; + children = ( + 61216B7F2667C79B0089DCD1 /* LogsTrackingConsentE2ETests.swift */, + 61216B7A2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift */, + 61B3BD51266128D300A9BEF0 /* LoggerE2ETests.swift */, + 61216B752666DDA10089DCD1 /* LoggerConfigurationTests.swift */, + ); + path = Logging; + sourceTree = ""; + }; + 61B5E41F26DF857E000B0A5F /* ObjcAPITests */ = { + isa = PBXGroup; + children = ( + 61B5E42626DFB145000B0A5F /* DDDatadog+apiTests.m */, + 61B5E42826DFB60A000B0A5F /* DDConfiguration+apiTests.m */, + 61B5E42026DF85C7000B0A5F /* DDRUMMonitor+apiTests.m */, + 61112F8D2A4417D6006FFCA6 /* DDRUM+apiTests.m */, + 6147989B2A459E2B0095CB02 /* DDTrace+apiTests.m */, + 61B5E42A26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m */, + D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */, + A79B0F63292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m */, + A728ADAD2934EB0300397996 /* DDW3CHTTPHeadersWriter+apiTests.m */, + 3C1890132ABDE99200CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m */, + A795069D2B974CAA00AC4814 /* DDSessionReplay+apiTests.m */, + 6174D6052BFB9D5500EC7469 /* DDWebViewTracking+apiTests.m */, + F603F12D2CAEA7590088E6B7 /* DDInternalLogger+apiTests.m */, + ); + path = ObjcAPITests; + sourceTree = ""; + }; + 61BAD46826415FA2001886CA /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 61BAD46926415FCE001886CA /* OTSpanTests.swift */, + ); + path = OpenTracing; + sourceTree = ""; + }; + 61C3637E2436163400C4D4E6 /* DatadogPrivate */ = { + isa = PBXGroup; + children = ( + 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */, + ); + path = DatadogPrivate; + sourceTree = ""; + }; + 61C3E63124BF143C008053F2 /* RUMMonitor */ = { + isa = PBXGroup; + children = ( + 61C713A92A3B790B00FA735A /* Monitor.swift */, + 61C3E63624BF191F008053F2 /* RUMScope.swift */, + 61C3E63A24BF1A4B008053F2 /* RUMCommand.swift */, + 61C3E63C24BF1B7F008053F2 /* Scopes */, + ); + path = RUMMonitor; + sourceTree = ""; + }; + 61C3E63C24BF1B7F008053F2 /* Scopes */ = { + isa = PBXGroup; + children = ( + 61494B7827F3522C0082BBCC /* Utils */, + 6194B92C2BB43F9C00179430 /* FatalErrorContextNotifier.swift */, + 6122514727FDFF82004F5AE4 /* RUMScopeDependencies.swift */, + 61C3E63D24BF1B91008053F2 /* RUMApplicationScope.swift */, + 61C2C20624C098FC00C0321C /* RUMSessionScope.swift */, + 61C2C21124C5951400C0321C /* RUMViewScope.swift */, + 61494CB024C839460082C633 /* RUMResourceScope.swift */, + 61494CB924CB126F0082C633 /* RUMUserActionScope.swift */, + ); + path = Scopes; + sourceTree = ""; + }; + 61C5A87A24509A0C00DA608C /* Utils */ = { + isa = PBXGroup; + children = ( + 61C5A87C24509A0C00DA608C /* Casting.swift */, + E1D202E924C065CF00D1AF3A /* ActiveSpansPool.swift */, + 61C5A87D24509A0C00DA608C /* Warnings.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61C5A89724509C1100DA608C /* Tracing */ = { + isa = PBXGroup; + children = ( + 3CF673352B4807490016CE17 /* OTelSpanTests.swift */, + 61AD4E3924534075006E34EA /* DatadogTraceFeatureTests.swift */, + D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */, + D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */, + ); + path = Tracing; + sourceTree = ""; + }; + 61C5A89924509C1100DA608C /* Utils */ = { + isa = PBXGroup; + children = ( + 61C5A89A24509C1100DA608C /* WarningsTests.swift */, + 61C5A89C24509C1100DA608C /* Casting+Tracing.swift */, + E1D203FB24C1884500D1AF3A /* ActiveSpansPoolTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61C5A8A324509FAA00DA608C /* Span */ = { + isa = PBXGroup; + children = ( + 61C5A8A424509FAA00DA608C /* SpanEventEncoder.swift */, + 61C5A8A524509FAA00DA608C /* SpanEventBuilder.swift */, + 61122ECD25B1B74500F9C7F5 /* SpanSanitizer.swift */, + 614872762485067300E3EBDB /* SpanTagsReducer.swift */, + 61CE58592B48174D00479510 /* SpanWriteContext.swift */, + ); + path = Span; + sourceTree = ""; + }; + 61C713BE2A3C9D9D00FA735A /* Feature */ = { + isa = PBXGroup; + children = ( + 61C713BF2A3C9DAD00FA735A /* RequestBuilderTests.swift */, + ); + path = Feature; + sourceTree = ""; + }; + 61C713C52A3CA08B00FA735A /* CoreMocks */ = { + isa = PBXGroup; + children = ( + A71265852B17980C007D63CE /* MockFeature.swift */, + D257954A298ABB04008A1BE5 /* PassthroughCoreMock.swift */, + D2160CEF29C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift */, + 61C713CF2A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift */, + 613F9C1A2BB03188007C7606 /* FeatureScopeMock.swift */, + ); + path = CoreMocks; + sourceTree = ""; + }; + 61D03BDE273404BB00367DE0 /* RUM */ = { + isa = PBXGroup; + children = ( + 61D03BDF273404E700367DE0 /* RUMDataModels+objcTests.swift */, + ); + path = RUM; + sourceTree = ""; + }; + 61D3E0C7277B237D008BE766 /* Kronos */ = { + isa = PBXGroup; + children = ( + 61D3E0CC277B23F0008BE766 /* KronosClock.swift */, + 61D3E0CD277B23F0008BE766 /* KronosData+Bytes.swift */, + 61D3E0C9277B23F0008BE766 /* KronosDNSResolver.swift */, + 61D3E0C8277B23F0008BE766 /* KronosInternetAddress.swift */, + 61D3E0D1277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift */, + 61D3E0CE277B23F0008BE766 /* KronosNTPClient.swift */, + 61D3E0CB277B23F0008BE766 /* KronosNTPPacket.swift */, + 61D3E0CF277B23F0008BE766 /* KronosNTPProtocol.swift */, + 61D3E0D0277B23F1008BE766 /* KronosTimeFreeze.swift */, + 61D3E0CA277B23F0008BE766 /* KronosTimeStorage.swift */, + ); + path = Kronos; + sourceTree = ""; + }; + 61D3E0DD277B3D6E008BE766 /* Kronos */ = { + isa = PBXGroup; + children = ( + 61D3E0DF277B3D92008BE766 /* KronosNTPPacketTests.swift */, + 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */, + 61B8BA90281812F60068AFF4 /* KronosInternetAddressTests.swift */, + ); + path = Kronos; + sourceTree = ""; + }; + 61D3E0E8277E0C16008BE766 /* NTP */ = { + isa = PBXGroup; + children = ( + 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */, + ); + path = NTP; + sourceTree = ""; + }; + 61DCC84C2C05D4E500CB59E5 /* SDKMetrics */ = { + isa = PBXGroup; + children = ( + 61DCC8492C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift */, + ); + path = SDKMetrics; + sourceTree = ""; + }; + 61E45BD02450F64100F2C652 /* Span */ = { + isa = PBXGroup; + children = ( + 61E45BD12450F65B00F2C652 /* SpanEventBuilderTests.swift */, + 61122EE725B1C92500F9C7F5 /* SpanSanitizerTests.swift */, + 618C0FBF2B482F6800266B38 /* SpanWriteContextTests.swift */, + ); + path = Span; + sourceTree = ""; + }; + 61E5332D24B75DC7003D6C4E /* RUM */ = { + isa = PBXGroup; + children = ( + 61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */, + 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */, + 617B954124BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift */, + 49274908288048F400ECD49B /* RUMInternalProxyTests.swift */, + 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */, + 61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */, + 61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */, + 61410166251A661D00E3C2D9 /* UIApplicationSwizzlerTests.swift */, + D29A9FDC29DDC704005C54A4 /* Integrations */, + 61FF282E24BC5E0E000B3D9B /* RUMEventOutputs */, + B3FC3C1226526F4100DEED9E /* RUMVitals */, + 61411B0F24EC15AC0012EAB2 /* Casting+RUM.swift */, + D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */, + ); + path = RUM; + sourceTree = ""; + }; + 61E5333224B7A504003D6C4E /* DataModels */ = { + isa = PBXGroup; + children = ( + 9E26E6B824C87693000B3270 /* RUMDataModels.swift */, + 618715F824DC13A100FC0F69 /* RUMDataModelsMapping.swift */, + ); + path = DataModels; + sourceTree = ""; + }; + 61E5333B24B87908003D6C4E /* RUMEvent */ = { + isa = PBXGroup; + children = ( + 614B0A4A24EBC43D00A2A780 /* RUMUser.swift */, + 61FF281D24B8968D000B3D9B /* RUMEventBuilder.swift */, + 61122ED325B1B84D00F9C7F5 /* RUMEventSanitizer.swift */, + 614B0A4E24EBDC6B00A2A780 /* RUMConnectivityInfoProvider.swift */, + 61FD9FCB28533EDF00214BD9 /* RUMDeviceInfo.swift */, + 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */, + ); + path = RUMEvent; + sourceTree = ""; + }; + 61E8C5062B28896100E709B4 /* RUM */ = { + isa = PBXGroup; + children = ( + 61E8C5072B28898800E709B4 /* StartingRUMSessionTests.swift */, + 6167E6DC2B811A8300C3CA2D /* AppHangsMonitoringTests.swift */, + 3CA00B062C2AE52400E6FE01 /* WatchdogTerminationsMonitoringTests.swift */, + D2552AF42BBC47D900A45725 /* WebEventIntegrationTests.swift */, + 61DCC84C2C05D4E500CB59E5 /* SDKMetrics */, + ); + path = RUM; + sourceTree = ""; + }; + 61E909E524A24DD3005EA2DE /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 61E909E624A24DD3005EA2DE /* OTSpan.swift */, + 61E909E724A24DD3005EA2DE /* OTFormat.swift */, + 61E909E924A24DD3005EA2DE /* OTTracer.swift */, + 61E909EA24A24DD3005EA2DE /* OTReference.swift */, + 61E909EB24A24DD3005EA2DE /* OTConstants.swift */, + 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */, + ); + path = OpenTracing; + sourceTree = ""; + }; + 61E917CD246426E000E6C631 /* Utils */ = { + isa = PBXGroup; + children = ( + 6139CD762589FEE3007E8BB7 /* RetryingTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61E945E5286C504B00A946C4 /* DD */ = { + isa = PBXGroup; + children = ( + 61DA8CB728647A500074A606 /* InternalLoggerTests.swift */, + ); + path = DD; + sourceTree = ""; + }; + 61F1A61B2498AD2C00075390 /* SystemFrameworks */ = { + isa = PBXGroup; + children = ( + 61133C1C2423990D00786299 /* UIKitMocks.swift */, + 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */, + D20FD9D22ACC08D1004D3569 /* WebKitMocks.swift */, + ); + path = SystemFrameworks; + sourceTree = ""; + }; + 61F2724725C9437C00D54BF8 /* CrashReporting */ = { + isa = PBXGroup; + children = ( + 61F2724825C943C500D54BF8 /* CrashReporterTests.swift */, + 61FC5F3325CC187D006BB4DE /* CrashContext */, + ); + path = CrashReporting; + sourceTree = ""; + }; + 61F2727225C9507C00D54BF8 /* PLCrashReporterIntegration */ = { + isa = PBXGroup; + children = ( + 6170DC1B25C18729003AED5C /* PLCrashReporterPlugin.swift */, + 612556AF268C8D31002BCE74 /* CrashReport.swift */, + 61FDBA1226971953001D9D43 /* CrashReportMinifier.swift */, + 612556BA268DD9BF002BCE74 /* DDCrashReportExporter.swift */, + 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */, + 61F2728A25C9561A00D54BF8 /* PLCrashReporterIntegration.swift */, + 615CC40A2694A55D0005F08C /* Utils */, + ); + path = PLCrashReporterIntegration; + sourceTree = ""; + }; + 61F3CDA1251118DD00C816E5 /* Views */ = { + isa = PBXGroup; + children = ( + D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */, + D249859B2727FF1D00B4F72D /* UIKit */, + D249859E2728042200B4F72D /* SwiftUI */, + ); + path = Views; + sourceTree = ""; + }; + 61F3CDA825121F8F00C816E5 /* Instrumentation */ = { + isa = PBXGroup; + children = ( + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */, + 61F3CDA925121FA100C816E5 /* Views */, + 6141014C251A577D00E3C2D9 /* Actions */, + 613F23EF252B1287006CD2D7 /* Resources */, + 6167E6D82B80047900C3CA2D /* AppHangs */, + 3CFF4F9C2C0DBEEA006F191D /* WatchdogTerminations */, + 61C713BB2A3C95AD00FA735A /* RUMInstrumentationTests.swift */, + ); + path = Instrumentation; + sourceTree = ""; + }; + 61F3CDA925121FA100C816E5 /* Views */ = { + isa = PBXGroup; + children = ( + D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */, + ); + path = Views; + sourceTree = ""; + }; + 61F3E36B2BC7D51400C7881E /* Trace */ = { + isa = PBXGroup; + children = ( + 61F3E36C2BC7D66700C7881E /* HeadBasedSamplingTests.swift */, + ); + path = Trace; + sourceTree = ""; + }; + 61F930BC2BA1A405005F0EE2 /* TLV */ = { + isa = PBXGroup; + children = ( + D29CDD3128211A2200F7DAA5 /* TLVBlock.swift */, + 61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */, + ); + path = TLV; + sourceTree = ""; + }; + 61F930C02BA1C306005F0EE2 /* TLV */ = { + isa = PBXGroup; + children = ( + D2B3F0432823EE8300C2B5EE /* TLVBlockTests.swift */, + 61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */, + ); + path = TLV; + sourceTree = ""; + }; + 61FC5F3325CC187D006BB4DE /* CrashContext */ = { + isa = PBXGroup; + children = ( + 61FC5F3425CC1898006BB4DE /* CrashContextProviderTests.swift */, + 6172472625D673D7007085B3 /* CrashContextTests.swift */, + ); + path = CrashContext; + sourceTree = ""; + }; + 61FCBFE525DBBE7D00CCF864 /* PLCrashReporterIntegration */ = { + isa = PBXGroup; + children = ( + D243BBBF276C9D640019C857 /* PLCrashReporterIntegrationTests.swift */, + 615CC4122695957C0005F08C /* CrashReportTests.swift */, + 61FDBA14269722B4001D9D43 /* CrashReportMinifierTests.swift */, + 61E95D872695C00200EA3115 /* DDCrashReportExporterTests.swift */, + 61FDBA1626974CA9001D9D43 /* DDCrashReportBuilderTests.swift */, + 615CC40E2694A63A0005F08C /* Utils */, + ); + path = PLCrashReporterIntegration; + sourceTree = ""; + }; + 61FF281F24B89807000B3D9B /* RUMEvent */ = { + isa = PBXGroup; + children = ( + 61FF282024B8981D000B3D9B /* RUMEventBuilderTests.swift */, + 61122EED25B1D75B00F9C7F5 /* RUMEventSanitizerTests.swift */, + 61FD9FCE28534EBD00214BD9 /* RUMDeviceInfoTests.swift */, + ); + path = RUMEvent; + sourceTree = ""; + }; + 61FF282E24BC5E0E000B3D9B /* RUMEventOutputs */ = { + isa = PBXGroup; + children = ( + 61FF282F24BC5E2D000B3D9B /* RUMEventFileOutputTests.swift */, + ); + 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 = ( + 9E359F4D26CD518D001E25E9 /* LongTaskObserver.swift */, + ); + path = LongTasks; + sourceTree = ""; + }; + 9E47010324471027000073A4 /* include */ = { + isa = PBXGroup; + children = ( + 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */, + 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */, + ); + path = include; + sourceTree = ""; + }; + 9E64849B27031050007CCD7B /* RUM */ = { + isa = PBXGroup; + children = ( + 9E5B6D2F270C85AB002499B8 /* RUMUtils.swift */, + 9E64849C27031071007CCD7B /* RUMGlobalE2ETests.swift */, + 9E5B6D2D270C84B4002499B8 /* RUMMonitorE2ETests.swift */, + 9E5B6D31270DE9E5002499B8 /* RUMTrackingConsentE2ETests.swift */, + ); + path = RUM; + sourceTree = ""; + }; + 9E68FB52244707FD0013A8AA /* DatadogPrivate */ = { + isa = PBXGroup; + children = ( + 9E47010324471027000073A4 /* include */, + 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */, + 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */, + ); + name = DatadogPrivate; + path = ../DatadogCore/Private; + sourceTree = ""; + }; + A728AD992934CE2800397996 /* W3C */ = { + isa = PBXGroup; + children = ( + A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */, + A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */, + A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */, + ); + path = W3C; + sourceTree = ""; + }; + A7B932F62B1F6A0A00AE6477 /* Models */ = { + isa = PBXGroup; + children = ( + A7B932F72B1F6A0A00AE6477 /* EnrichedRecord.swift */, + A7B932F82B1F6A0A00AE6477 /* SRDataModels.swift */, + A7B932F92B1F6A0A00AE6477 /* EnrichedResource.swift */, + A7B932FA2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift */, + ); + path = Models; + sourceTree = ""; + }; + A7F773D929253F5900AC1A62 /* Datadog */ = { + isa = PBXGroup; + children = ( + 618E13B02524B8F80098C6B0 /* TracingHTTPHeaders.swift */, + 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */, + 618E13A92524B8700098C6B0 /* HTTPHeadersReader.swift */, + ); + path = Datadog; + sourceTree = ""; + }; + A7F773DA29253F6200AC1A62 /* B3 */ = { + isa = PBXGroup; + children = ( + A7F773D32924EA2D00AC1A62 /* B3HTTPHeaders.swift */, + A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */, + A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */, + ); + path = B3; + sourceTree = ""; + }; + B3FC3C0426526EE900DEED9E /* RUMVitals */ = { + isa = PBXGroup; + children = ( + 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */, + 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */, + B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */, + B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */, + 9E9973F0268DF69500D8059B /* VitalInfoSampler.swift */, + E179FB4D28F80A6400CC2698 /* PerformanceMetric.swift */, + ); + path = RUMVitals; + sourceTree = ""; + }; + B3FC3C1226526F4100DEED9E /* RUMVitals */ = { + isa = PBXGroup; + children = ( + B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */, + 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTests.swift */, + B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */, + 9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */, + 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */, + ); + path = RUMVitals; + sourceTree = ""; + }; + D20605B4287572270047275C /* DatadogCore */ = { + isa = PBXGroup; + children = ( + D20605B5287572640047275C /* DatadogContextProviderMock.swift */, + D20605B82875729E0047275C /* ContextValuePublisherMock.swift */, + ); + path = DatadogCore; + sourceTree = ""; + }; + D207317D29A5226A00ECBF94 /* DatadogLogs */ = { + isa = PBXGroup; + children = ( + D2D30E5A2A40BF540020C553 /* Logs.swift */, + 49D8C0B92AC5F21F0075E427 /* Logs+Internal.swift */, + D24C9C3E29A79772002057CF /* Logger.swift */, + D243BBEB29A614CE000B9CEC /* LoggerProtocol.swift */, + D2B249932A4598FE00DD4F9F /* LoggerProtocol+Internal.swift */, + 6194E4B828785BFD00EB6307 /* RemoteLogger.swift */, + 6194E4BB2878AF7600EB6307 /* ConsoleLogger.swift */, + D24C9C4129A7986E002057CF /* Feature */, + 61133BC12423979B00786299 /* Log */, + D22C1F5A2714849700922024 /* Scrubbing */, + ); + name = DatadogLogs; + path = ../DatadogLogs/Sources; + sourceTree = ""; + }; + D207318729A5226B00ECBF94 /* DatadogLogsTests */ = { + isa = PBXGroup; + children = ( + D2D30E5D2A40CD2C0020C553 /* LogsTests.swift */, + D2B249962A45E10500DD4F9F /* LoggerTests.swift */, + 6194D51B287ECDC00091547D /* ConsoleLoggerTests.swift */, + D21C26EA28AFA11E005DD405 /* LogMessageReceiverTests.swift */, + D242C2A02A14D747004B4980 /* RemoteLoggerTests.swift */, + D20FD9CE2AC6FF42004D3569 /* WebViewLogReceiverTests.swift */, + 61133C3A2423990D00786299 /* Log */, + D2A783EC29A534DB003B03BB /* Mocks */, + ); + name = DatadogLogsTests; + path = ../DatadogLogs/Tests; + sourceTree = ""; + }; + D2160CC029C0DED100FAA9A5 /* URLSession */ = { + isa = PBXGroup; + children = ( + 614A708D2BF754D700D9AF42 /* ImmutableRequest.swift */, + D295A16429F299C9007C0E9A /* URLSessionInterceptor.swift */, + D2160CC129C0DED100FAA9A5 /* URLSessionTaskInterception.swift */, + 3CBDE6732AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift */, + D2160CC329C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift */, + 3CBDE6892AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift */, + D2181A8A2B0500BB00A518C0 /* NetworkInstrumentationSwizzler.swift */, + D2BEEDB42B33607D0065F3AC /* URLSessionSwizzler.swift */, + D2BEEDAB2B3356710065F3AC /* URLSessionTaskSwizzler.swift */, + D2BEEDB12B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift */, + D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */, + ); + path = URLSession; + sourceTree = ""; + }; + D2160CE229C0DFED00FAA9A5 /* Swizzling */ = { + isa = PBXGroup; + children = ( + D2160CE329C0DFED00FAA9A5 /* MethodSwizzler.swift */, + ); + path = Swizzling; + sourceTree = ""; + }; + D2160CE729C0E00200FAA9A5 /* Swizzling */ = { + isa = PBXGroup; + children = ( + D2160CE829C0E00200FAA9A5 /* MethodSwizzlerTests.swift */, + ); + path = Swizzling; + sourceTree = ""; + }; + D21AE6BA29E5ED7D0064BF29 /* Telemetry */ = { + isa = PBXGroup; + children = ( + D21AE6BB29E5EDAF0064BF29 /* TelemetryTests.swift */, + E1C853132AA9B9A300C74BCF /* TelemetryMocks.swift */, + ); + path = Telemetry; + sourceTree = ""; + }; + D21C26E928AF9D22005DD405 /* Integrations */ = { + isa = PBXGroup; + children = ( + 61216275247D1CD700AC5D67 /* TracingWithLoggingIntegration.swift */, + D25BADA029C1EF3000112069 /* TracingURLSessionHandler.swift */, + ); + path = Integrations; + sourceTree = ""; + }; + D227A0A22C76229400C83324 /* Benchmarks */ = { + isa = PBXGroup; + children = ( + D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */, + ); + path = Benchmarks; + sourceTree = ""; + }; + D22C1F5A2714849700922024 /* Scrubbing */ = { + isa = PBXGroup; + children = ( + D22C1F5B271484B400922024 /* LogEventMapper.swift */, + ); + path = Scrubbing; + sourceTree = ""; + }; + D23039A6298D513D001A1FA3 /* DatadogInternal */ = { + isa = PBXGroup; + children = ( + D227A0A22C76229400C83324 /* Benchmarks */, + 6167E6DF2B81203A00C3CA2D /* Models */, + D23039CA298D5235001A1FA3 /* Attributes */, + D23039C3298D5235001A1FA3 /* Codable */, + D23039DA298D5235001A1FA3 /* Concurrency */, + D23039B2298D5235001A1FA3 /* Context */, + D23039B1298D5235001A1FA3 /* DatadogCoreProtocol.swift */, + D23039BD298D5235001A1FA3 /* DatadogFeature.swift */, + D2DE63522A30A7CA00441A54 /* CoreRegistry.swift */, + 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */, + D23039AD298D5234001A1FA3 /* DD.swift */, + D23039D6298D5235001A1FA3 /* Extensions */, + D23039BF298D5235001A1FA3 /* MessageBus */, + D23039AE298D5235001A1FA3 /* Storage */, + 6128F5682BA2237300D35B08 /* DataStore */, + D23039CD298D5235001A1FA3 /* Telemetry */, + 6167E6FB2B81EBD100C3CA2D /* BacktraceReporting */, + D23039D1298D5235001A1FA3 /* Upload */, + D2A783D329A53049003B03BB /* Utils */, + D2160CE229C0DFED00FAA9A5 /* Swizzling */, + D2EBEE1D29BA15BC00B15732 /* NetworkInstrumentation */, + 6174D60A2BFDDE1900EC7469 /* SDKMetrics */, + ); + name = DatadogInternal; + path = ../DatadogInternal/Sources; + sourceTree = ""; + }; + D23039AE298D5235001A1FA3 /* Storage */ = { + isa = PBXGroup; + children = ( + D263BCAE29DAFFEB00FA0E21 /* PerformancePresetOverride.swift */, + D23039AF298D5235001A1FA3 /* Writer.swift */, + ); + path = Storage; + sourceTree = ""; + }; + D23039B2298D5235001A1FA3 /* Context */ = { + isa = PBXGroup; + children = ( + E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */, + D23039B3298D5235001A1FA3 /* AppState.swift */, + D23039B4298D5235001A1FA3 /* UserInfo.swift */, + D23039B5298D5235001A1FA3 /* BatteryStatus.swift */, + D23039B6298D5235001A1FA3 /* CarrierInfo.swift */, + D23039B7298D5235001A1FA3 /* DateProvider.swift */, + D23039B8298D5235001A1FA3 /* Sysctl.swift */, + D23039B9298D5235001A1FA3 /* NetworkConnectionInfo.swift */, + D23039BA298D5235001A1FA3 /* DatadogContext.swift */, + D23039BB298D5235001A1FA3 /* TrackingConsent.swift */, + D23039BC298D5235001A1FA3 /* DeviceInfo.swift */, + D23039BE298D5235001A1FA3 /* LaunchTime.swift */, + D2F8235229915E12003C7E99 /* DatadogSite.swift */, + 6174D6122BFDF16C00EC7469 /* BundleType.swift */, + ); + path = Context; + sourceTree = ""; + }; + D23039BF298D5235001A1FA3 /* MessageBus */ = { + isa = PBXGroup; + children = ( + D2216EBF2A94DE2800ADAEC8 /* FeatureBaggage.swift */, + D23039C1298D5235001A1FA3 /* FeatureMessageReceiver.swift */, + D23039C2298D5235001A1FA3 /* FeatureMessage.swift */, + ); + path = MessageBus; + sourceTree = ""; + }; + D23039C3298D5235001A1FA3 /* Codable */ = { + isa = PBXGroup; + children = ( + D23039C4298D5235001A1FA3 /* AnyEncoder.swift */, + D23039C6298D5235001A1FA3 /* AnyDecoder.swift */, + D23039C7298D5235001A1FA3 /* DynamicCodingKey.swift */, + D23039C8298D5235001A1FA3 /* AnyCodable.swift */, + D23039C9298D5235001A1FA3 /* AnyEncodable.swift */, + D23039C5298D5235001A1FA3 /* AnyDecodable.swift */, + ); + path = Codable; + sourceTree = ""; + }; + D23039CA298D5235001A1FA3 /* Attributes */ = { + isa = PBXGroup; + children = ( + D23039CB298D5235001A1FA3 /* Attributes.swift */, + D23039CC298D5235001A1FA3 /* AttributesSanitizer.swift */, + ); + path = Attributes; + sourceTree = ""; + }; + D23039CD298D5235001A1FA3 /* Telemetry */ = { + isa = PBXGroup; + children = ( + D23039CE298D5235001A1FA3 /* InternalLogger.swift */, + D23039CF298D5235001A1FA3 /* CoreLogger.swift */, + D23039D0298D5235001A1FA3 /* Telemetry.swift */, + ); + path = Telemetry; + sourceTree = ""; + }; + D23039D1298D5235001A1FA3 /* Upload */ = { + isa = PBXGroup; + children = ( + D26C49B52889416300802B2D /* UploadPerformancePreset.swift */, + D23039D2298D5235001A1FA3 /* URLRequestBuilder.swift */, + D23039D3298D5235001A1FA3 /* DataFormat.swift */, + D23039D4298D5235001A1FA3 /* DataCompression.swift */, + D23039D5298D5235001A1FA3 /* FeatureRequestBuilder.swift */, + D2D3199929E98D970004F169 /* DefaultJSONEncoder.swift */, + 3C0D5DD62A543B3B00446CF9 /* Event.swift */, + ); + path = Upload; + sourceTree = ""; + }; + D23039D6298D5235001A1FA3 /* Extensions */ = { + isa = PBXGroup; + children = ( + 3C3C9E2B2C64F3CA003AF22F /* Data+Crypto.swift */, + D23039D8298D5235001A1FA3 /* DatadogExtended.swift */, + D23354FB2A42E32000AFCAE2 /* InternalExtended.swift */, + D23039D7298D5235001A1FA3 /* Foundation+Datadog.swift */, + D22F06D529DAFD500026CC3C /* FixedWidthInteger+Convenience.swift */, + D22F06D629DAFD500026CC3C /* TimeInterval+Convenience.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + D23039DA298D5235001A1FA3 /* Concurrency */ = { + isa = PBXGroup; + children = ( + D23039DB298D5235001A1FA3 /* ReadWriteLock.swift */, + D2432CF829EDB22C00D93657 /* Flushable.swift */, + ); + path = Concurrency; + sourceTree = ""; + }; + D236BE2A29521A7700676E67 /* Integrations */ = { + isa = PBXGroup; + children = ( + 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */, + D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */, + 9E53889B2773C4B300A7DC42 /* WebViewEventReceiverTests.swift */, + D248ED4728081B9B00B315B4 /* TelemetryReceiverTests.swift */, + 61C453492C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift */, + ); + path = Integrations; + sourceTree = ""; + }; + D249859B2727FF1D00B4F72D /* UIKit */ = { + isa = PBXGroup; + children = ( + 61F3CDA62512144600C816E5 /* UIKitRUMViewsPredicate.swift */, + 61F3CDA2251118FB00C816E5 /* UIViewControllerHandler.swift */, + 61F3CDA42511190E00C816E5 /* UIViewControllerSwizzler.swift */, + ); + path = UIKit; + sourceTree = ""; + }; + D249859E2728042200B4F72D /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D249859F2728042200B4F72D /* SwiftUIViewModifier.swift */, + D24985A12728048B00B4F72D /* SwiftUIViewHandler.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + D24C9C4129A7986E002057CF /* Feature */ = { + isa = PBXGroup; + children = ( + 616F1FAF283E227100651A3A /* LogsFeature.swift */, + D243BBF129A6209C000B9CEC /* RequestBuilder.swift */, + D243BBF429A620CC000B9CEC /* MessageReceivers.swift */, + D22C5BC52A989D130024CC1F /* Baggages.swift */, + ); + path = Feature; + sourceTree = ""; + }; + D24C9C5F29A7CAB0002057CF /* Logs */ = { + isa = PBXGroup; + children = ( + 61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */, + 61FF416125EE5FF400CE35EC /* CrashLogReceiverTests.swift */, + ); + path = Logs; + sourceTree = ""; + }; + D24CA32F28B3BF0C007B26BF /* Integrations */ = { + isa = PBXGroup; + children = ( + 6112B11325C84E7900B37771 /* CrashReportSender.swift */, + 6167E7022B81F2EB00C3CA2D /* BacktraceReporter.swift */, + ); + path = Integrations; + sourceTree = ""; + }; + D2546C0629AF55CE0054E00B /* Feature */ = { + isa = PBXGroup; + children = ( + D2546C0329AF55AA0054E00B /* TraceFeature.swift */, + D2546C0729AF55E90054E00B /* RequestBuilder.swift */, + D2546C0A29AF56270054E00B /* MessageReceivers.swift */, + D22C5BCA2A98A5400024CC1F /* Baggages.swift */, + ); + path = Feature; + sourceTree = ""; + }; + D257953F298ABA65008A1BE5 /* TestUtilities */ = { + isa = PBXGroup; + children = ( + 61AE74112AD6EE7E008DB9BB /* Matchers */, + D257954D298ABB04008A1BE5 /* Helpers */, + D2579546298ABB04008A1BE5 /* Mocks */, + ); + name = TestUtilities; + path = ../TestUtilities; + sourceTree = ""; + }; + D2579546298ABB04008A1BE5 /* Mocks */ = { + isa = PBXGroup; + children = ( + 6167E7162B837F4200C3CA2D /* FeatureModels */, + 6188900D2AC58B5D00D0B966 /* MessageReceiverMocks */, + 61C713C52A3CA08B00FA735A /* CoreMocks */, + 3C85D42B29F7C87D00AFF894 /* HostsSanitizerMock.swift */, + D2579547298ABB04008A1BE5 /* FileWriterMock.swift */, + D2579548298ABB04008A1BE5 /* DatadogContextMock.swift */, + D2579549298ABB04008A1BE5 /* FeatureBaggageMock.swift */, + 61AE74162AD7DA9B008DB9BB /* FeatureMessageMocks.swift */, + D257954B298ABB04008A1BE5 /* FoundationMocks.swift */, + D257954C298ABB04008A1BE5 /* AttributesMocks.swift */, + D2DA23C6298D5AC000C6C7E6 /* TelemetryMocks.swift */, + 6167E7112B837F0B00C3CA2D /* BacktraceReportingMocks.swift */, + 615B0F8D2BB33E0400E9ED6C /* DataStoreMock.swift */, + D2DA23C9298D5C1300C6C7E6 /* UIKitMocks.swift */, + D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */, + D24C9C5129A7BD12002057CF /* SamplerMock.swift */, + D24C9C5429A7C5F3002057CF /* DateProvider.swift */, + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */, + D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */, + D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */, + 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, + D2160CF629C0EE2B00FAA9A5 /* UploadMocks.swift */, + 3C0D5DEE2A5442A900446CF9 /* EventMocks.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + D257954D298ABB04008A1BE5 /* Helpers */ = { + isa = PBXGroup; + children = ( + D257954E298ABB04008A1BE5 /* Encoding.swift */, + D257954F298ABB04008A1BE5 /* DDAssert.swift */, + D2579550298ABB04008A1BE5 /* SwiftExtensions.swift */, + D2579551298ABB04008A1BE5 /* XCTestCase.swift */, + D2F44FBB299AA36D0074B0D9 /* Decompression.swift */, + 61133C462423990D00786299 /* TestsDirectory.swift */, + 61C713D22A3DFB4900FA735A /* FuzzyHelpers.swift */, + 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */, + 6174D61C2C007B3300EC7469 /* ModuleName.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + D25C834D2B88A261008E73B1 /* WebViewTracking */ = { + isa = PBXGroup; + children = ( + D21A94F12B8397CA00AC4256 /* WebViewMessage.swift */, + ); + path = WebViewTracking; + sourceTree = ""; + }; + D25EE93529C4C3C300CE3839 /* DatadogTrace */ = { + isa = PBXGroup; + children = ( + 3C6C7FDE2B459AAA006F5CBC /* OpenTelemetry */, + 61A2CC382A44B0EA0000FF25 /* Trace.swift */, + 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */, + 61A2CC3B2A44BED30000FF25 /* Tracer.swift */, + D2546BF029AF4F550054E00B /* DatadogTracer.swift */, + 61C5A87924509A0C00DA608C /* DDNoOps.swift */, + 61C5A87824509A0C00DA608C /* DDSpan.swift */, + 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */, + D2EBEDD629B8F08E00B15732 /* DDFormat.swift */, + 61E909E524A24DD3005EA2DE /* OpenTracing */, + D2546C0629AF55CE0054E00B /* Feature */, + 61C5A8A324509FAA00DA608C /* Span */, + 618D9DE5263AD77600A3FAD2 /* Scrubbing */, + D21C26E928AF9D22005DD405 /* Integrations */, + 61C5A87A24509A0C00DA608C /* Utils */, + ); + name = DatadogTrace; + path = ../DatadogTrace/Sources; + sourceTree = ""; + }; + D25EE93F29C4C3C400CE3839 /* DatadogTraceTests */ = { + isa = PBXGroup; + children = ( + 3C6C7FF12B459AB3006F5CBC /* OpenTelemetry */, + 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */, + 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, + 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */, + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */, + 61C5A89824509C1100DA608C /* DDSpanTests.swift */, + 61F1A620249A45E400075390 /* DDSpanContextTests.swift */, + D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */, + D2E8D59728C7AB90007E5DE1 /* ContextMessageReceiverTests.swift */, + D2A38DDA29C37E1B007C6900 /* TracingURLSessionHandlerTests.swift */, + 61E45BD02450F64100F2C652 /* Span */, + 61C5A89924509C1100DA608C /* Utils */, + 619CE7602A458D66005588CB /* TraceTests.swift */, + ); + name = DatadogTraceTests; + path = ../DatadogTrace/Tests; + sourceTree = ""; + }; + D25FF2E629CC6B320063802D /* Feature */ = { + isa = PBXGroup; + children = ( + D25FF2E729CC6B680063802D /* RUMFeature.swift */, + D25FF2ED29CC73240063802D /* RequestBuilder.swift */, + D25FF2F329CC88060063802D /* RUMBaggageKeys.swift */, + 3C0D5DEB2A54405A00446CF9 /* RUMViewEventsFilter.swift */, + 6194B9292BB4116A00179430 /* RUMDataStore.swift */, + ); + path = Feature; + sourceTree = ""; + }; + D263BCB129DB014900FA0E21 /* Extensions */ = { + isa = PBXGroup; + children = ( + 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */, + D263BCB229DB014900FA0E21 /* FixedWidthInteger+ConvenienceTests.swift */, + D263BCB329DB014900FA0E21 /* TimeInterval+ConvenienceTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + D26C49B428893E5300802B2D /* Upload */ = { + isa = PBXGroup; + children = ( + A70A82642A935F210072F5DC /* BackgroundTaskCoordinator.swift */, + D26C49BE288982DA00802B2D /* FeatureUpload.swift */, + 61133BB32423979B00786299 /* DataUploadDelay.swift */, + 61ED39D326C2A36B002C0F26 /* DataUploadStatus.swift */, + 61133BB12423979B00786299 /* DataUploadWorker.swift */, + 61133BB02423979B00786299 /* DataUploader.swift */, + 61133BAF2423979B00786299 /* DataUploadConditions.swift */, + 61133BB22423979B00786299 /* URLSessionClient.swift */, + 617699172A860D9D0030022B /* HTTPClient.swift */, + ); + path = Upload; + sourceTree = ""; + }; + D26F59312851E30B0097C455 /* DatadogInternal */ = { + isa = PBXGroup; + children = ( + 61A1A44829643254007909E7 /* DatadogCoreProxy.swift */, + D2553806288AA84F00727FAD /* UploadMock.swift */, + D250850F2976E30000E931C3 /* DatadogRemoteFeatureMock.swift */, + ); + path = DatadogInternal; + sourceTree = ""; + }; + D29A9F3529DD84AA005C54A4 /* DatadogRUM */ = { + isa = PBXGroup; + children = ( + D2A7A8FE2BA1C24A00F46845 /* PrivacyInfo.xcprivacy */, + 61C713B82A3C935C00FA735A /* RUM.swift */, + 49D8C0B62AC5D2160075E427 /* RUM+Internal.swift */, + 61C713A02A3B78F900FA735A /* RUMMonitorProtocol.swift */, + 61C713A22A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift */, + 61C713A12A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift */, + 61E5333524B84B43003D6C4E /* RUMMonitor.swift */, + D25FF2EA29CC6D6F0063802D /* RUMConfiguration.swift */, + 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */, + D25FF2E629CC6B320063802D /* Feature */, + B3FC3C0426526EE900DEED9E /* RUMVitals */, + 61E5333224B7A504003D6C4E /* DataModels */, + 61C3E63124BF143C008053F2 /* RUMMonitor */, + 6156CB8A24DDA186008CB2B2 /* RUMContext */, + 61E5333B24B87908003D6C4E /* RUMEvent */, + 613E81EE25A73FB90084B751 /* Scrubbing */, + 616CCE11250A181C009FED46 /* Instrumentation */, + 618DCFD524C7264100589570 /* UUIDs */, + 615950EC291C057D00470E0C /* Integrations */, + 61B22E5824F3E6A700DC26D2 /* Debugging */, + D29A9F8B29DD860A005C54A4 /* Utils */, + 6174D60E2BFDEA1F00EC7469 /* SDKMetrics */, + ); + name = DatadogRUM; + path = ../DatadogRUM/Sources; + sourceTree = ""; + }; + D29A9F3F29DD84AB005C54A4 /* DatadogRUMTests */ = { + isa = PBXGroup; + children = ( + 3C0D5DE62A543E9700446CF9 /* RUMViewEventsFilterTests.swift */, + 61C713AC2A3B793E00FA735A /* RUMMonitorProtocolTests.swift */, + 61C713B52A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift */, + 61C713B22A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift */, + D29A9FCB29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift */, + D29A9FE129DDC75E005C54A4 /* Mocks */, + 61C713BE2A3C9D9D00FA735A /* Feature */, + 618715FA24DC5EE700FC0F69 /* DataModels */, + 617B953B24BF4D7300E6F443 /* RUMMonitor */, + 61FF281F24B89807000B3D9B /* RUMEvent */, + 613E81F525A743470084B751 /* Scrubbing */, + 61F3CDA825121F8F00C816E5 /* Instrumentation */, + D236BE2A29521A7700676E67 /* Integrations */, + 61411B0E24EC15940012EAB2 /* Utils */, + 61C713C92A3DC22700FA735A /* RUMTests.swift */, + 6188697B2A4376F700E8996B /* RUMConfigurationTests.swift */, + 6174D6182BFE447600EC7469 /* SDKMetrics */, + ); + name = DatadogRUMTests; + path = ../DatadogRUM/Tests; + sourceTree = ""; + }; + D29A9F8B29DD860A005C54A4 /* Utils */ = { + isa = PBXGroup; + children = ( + 615F197B25B5A64B00BE14B5 /* UIKitExtensions.swift */, + D2FCA238271D896E0020286F /* SwiftUIExtensions.swift */, + 611529A425E3DD51004F740E /* ValuePublisher.swift */, + ); + path = Utils; + sourceTree = ""; + }; + D29A9FDC29DDC704005C54A4 /* Integrations */ = { + isa = PBXGroup; + children = ( + D224430C29E95D6600274EC7 /* CrashReportReceiverTests.swift */, + ); + path = Integrations; + sourceTree = ""; + }; + D29A9FE129DDC75E005C54A4 /* Mocks */ = { + isa = PBXGroup; + children = ( + 61E5333024B75DFC003D6C4E /* RUMFeatureMocks.swift */, + 613E820425A879AF0084B751 /* RUMDataModelMocks.swift */, + D29A9FDF29DDC75A005C54A4 /* UIKitMocks.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + D29D5A4A273BF81500A687C1 /* UIKit */ = { + isa = PBXGroup; + children = ( + 6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */, + 6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */, + F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */, + ); + path = UIKit; + sourceTree = ""; + }; + D29D5A4B273BF82200A687C1 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + D2A434A62A8E3FEA0028E329 /* Logs */ = { + isa = PBXGroup; + children = ( + 61133C0C2423983800786299 /* Logs+objc.swift */, + F6E106532C75E0D000716DC6 /* LogsDataModels+objc.swift */, + ); + path = Logs; + sourceTree = ""; + }; + D2A783D329A53049003B03BB /* Utils */ = { + isa = PBXGroup; + children = ( + D23039D9298D5235001A1FA3 /* DateFormatting.swift */, + 613C6B8F2768FDDE00870CBF /* Sampler.swift */, + D23039DC298D5235001A1FA3 /* DDError.swift */, + E2AA55E92C32C76A002FEF28 /* WatchKitExtensions.swift */, + 61133BBA2423979B00786299 /* SwiftExtensions.swift */, + D29A9F9429DDB1DB005C54A4 /* UIKitExtensions.swift */, + ); + path = Utils; + sourceTree = ""; + }; + D2A783D629A530B4003B03BB /* Utils */ = { + isa = PBXGroup; + children = ( + B3C27A072CE6342C006580F9 /* DeterministicSamplerTests.swift */, + D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */, + 116F84052CFDD06700705755 /* SampleRateTests.swift */, + 613C6B912768FF3100870CBF /* SamplerTests.swift */, + 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + D2A783D729A530CB003B03BB /* Concurrency */ = { + isa = PBXGroup; + children = ( + D2DA2395298D58F300C6C7E6 /* ReadWriteLockTests.swift */, + ); + path = Concurrency; + sourceTree = ""; + }; + D2A783D829A530DC003B03BB /* MessageBus */ = { + isa = PBXGroup; + children = ( + D2216EC22A96632F00ADAEC8 /* FeatureBaggageTests.swift */, + D2DA23A0298D58F400C6C7E6 /* FeatureMessageReceiverTests.swift */, + ); + path = MessageBus; + sourceTree = ""; + }; + D2A783EC29A534DB003B03BB /* Mocks */ = { + isa = PBXGroup; + children = ( + 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + D2A7841429A53B92003B03BB /* Files */ = { + isa = PBXGroup; + children = ( + 61133BAB2423979B00786299 /* Directory.swift */, + 61133BAC2423979B00786299 /* File.swift */, + ); + 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 = ( + 61AE740F2AD6EE4E008DB9BB /* WebViewMessageTests.swift */, + ); + path = Models; + sourceTree = ""; + }; + D2C5D52E2B84F6E700B63F36 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D2C5D52F2B84F71200B63F36 /* WebRecordIntegrationTests.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; + D2DA238B298D588A00C6C7E6 /* DatadogInternalTests */ = { + isa = PBXGroup; + children = ( + D2D36DCA2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift */, + D26416B52A30E84F00BCD9F7 /* CoreRegistryTest.swift */, + D2A783D729A530CB003B03BB /* Concurrency */, + D2F44FBA299AA2310074B0D9 /* Upload */, + D2DA2397298D58F300C6C7E6 /* Codable */, + D2DA239C298D58F300C6C7E6 /* Context */, + D21AE6BA29E5ED7D0064BF29 /* Telemetry */, + D2A783D629A530B4003B03BB /* Utils */, + D2A783D829A530DC003B03BB /* MessageBus */, + D2160CE729C0E00200FAA9A5 /* Swizzling */, + D2EBEE3A29BA162900B15732 /* NetworkInstrumentation */, + D263BCB129DB014900FA0E21 /* Extensions */, + D2C5D5272B83FD3700B63F36 /* Models */, + ); + name = DatadogInternalTests; + path = ../DatadogInternal/Tests; + sourceTree = ""; + }; + D2DA2397298D58F300C6C7E6 /* Codable */ = { + isa = PBXGroup; + children = ( + D2DA2398298D58F300C6C7E6 /* AnyEncodableTests.swift */, + D2DA2399298D58F300C6C7E6 /* AnyCodableTests.swift */, + D2DA239A298D58F300C6C7E6 /* AnyDecodableTests.swift */, + D2DA239B298D58F300C6C7E6 /* AnyCoderTests.swift */, + ); + path = Codable; + sourceTree = ""; + }; + D2DA239C298D58F300C6C7E6 /* Context */ = { + isa = PBXGroup; + children = ( + D2DA239D298D58F300C6C7E6 /* AppStateHistoryTests.swift */, + D2DA239E298D58F300C6C7E6 /* DeviceInfoTests.swift */, + 6174D6152BFDF29B00EC7469 /* BundleTypeTests.swift */, + ); + path = Context; + sourceTree = ""; + }; + D2EA0F442C0E1A8700CB20F8 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D2EA0F452C0E1AE200CB20F8 /* SessionReplayConfiguration.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; + D2EBEE1D29BA15BC00B15732 /* NetworkInstrumentation */ = { + isa = PBXGroup; + children = ( + D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */, + D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */, + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */, + 3CA8525E2BF2073800B52CBA /* TraceContextInjection.swift */, + D2EBEDCC29B893D800B15732 /* TraceID.swift */, + 3C9B27242B9F174700569C07 /* SpanID.swift */, + D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */, + D2160C9729C0DE5700FAA9A5 /* HostsSanitizer.swift */, + D2160C9629C0DE5600FAA9A5 /* TracingHeaderType.swift */, + D2EBEDCF29B8A02100B15732 /* TracePropagationHeadersWriter.swift */, + D2EBEDD229B8A58E00B15732 /* TracePropagationHeadersReader.swift */, + D2160CC029C0DED100FAA9A5 /* URLSession */, + A728AD992934CE2800397996 /* W3C */, + A7F773DA29253F6200AC1A62 /* B3 */, + A7F773D929253F5900AC1A62 /* Datadog */, + ); + path = NetworkInstrumentation; + sourceTree = ""; + }; + D2EBEE3A29BA162900B15732 /* NetworkInstrumentation */ = { + isa = PBXGroup; + children = ( + D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */, + D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */, + D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */, + D2160CD329C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift */, + D2160CD229C0DF6700FAA9A5 /* URLSessionTaskInterceptionTests.swift */, + D28ABFD52CECDE6B00623F27 /* URLSessionInterceptorTests.swift */, + 61E45BCE2450A6EC00F2C652 /* TraceIDTests.swift */, + 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */, + 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */, + 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */, + 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */, + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */, + A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */, + A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */, + A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */, + A728ADA52934DF2400397996 /* W3CHTTPHeadersReaderTests.swift */, + D2181A8D2B051B7900A518C0 /* URLSessionSwizzlerTests.swift */, + D2BEEDAE2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift */, + D2BEEDB72B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift */, + D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */, + 6156A9062BF75A7C00DF66C3 /* ImmutableRequestTests.swift */, + ); + path = NetworkInstrumentation; + sourceTree = ""; + }; + D2EFA866286DA82700F1FAA6 /* Context */ = { + isa = PBXGroup; + children = ( + D2EFA867286DA85700F1FAA6 /* DatadogContextProvider.swift */, + D20605A2287464F40047275C /* ContextValuePublisher.swift */, + D20605A5287476230047275C /* ServerOffsetPublisher.swift */, + D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */, + D20605B12874E1660047275C /* CarrierInfoPublisher.swift */, + D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */, + D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */, + D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */, + D2553825288F0B1A00727FAD /* BatteryStatusPublisher.swift */, + D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */, + D29294DF291D5ECD00F8EFF9 /* ApplicationVersionPublisher.swift */, + D2FB1253292E0E92005B13F8 /* TrackingConsentPublisher.swift */, + ); + path = Context; + sourceTree = ""; + }; + D2EFA873286E010100F1FAA6 /* DatadogCore */ = { + isa = PBXGroup; + children = ( + 617699162A8608C20030022B /* Context */, + 614B78EA296D7B63009C6B92 /* DatadogCoreTests.swift */, + 6167E70D2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift */, + 613F9C172BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift */, + D21C26D028A64599005DD405 /* MessageBusTests.swift */, + ); + path = DatadogCore; + sourceTree = ""; + }; + D2F44FBA299AA2310074B0D9 /* Upload */ = { + isa = PBXGroup; + children = ( + 3C0D5DF42A5443B100446CF9 /* DataFormatTests.swift */, + D213532F270CA722000315AD /* DataCompressionTests.swift */, + ); + path = Upload; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3CE119F929F7BE0000202522 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133B7D242393DE00786299 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 61133B93242393DE00786299 /* DatadogCore.h in Headers */, + 6179FFDE254ADBEF00556A0B /* ObjcAppLaunchHandler.h in Headers */, + 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133BEB242397DA00786299 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 61133C00242397DA00786299 /* DatadogObjc.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6133D1E82A6ED9E100384BEF /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7884F25C180CB002675B5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 61B7886425C180CB002675B5 /* DatadogCrashReporting.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207317729A5226A00ECBF94 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D20731A729A5279D00ECBF94 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23039A0298D513C001A1FA3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8E5029DDCD28001CFAE8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2579539298ABA65008A1BE5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D230399A298D50F1001A1FA3 /* XCTest.framework in Headers */, + D230399B298D50F1001A1FA3 /* DatadogCore.framework in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2579576298ABB83008A1BE5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D230399C298D50F1001A1FA3 /* XCTest.framework in Headers */, + D230399D298D50F1001A1FA3 /* DatadogCore.framework in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE92F29C4C3C300CE3839 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F2F29DD84AA005C54A4 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A53629C4F2DF00946C31 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6E0B27C50EAE00A62B57 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CB6E0C27C50EAE00A62B57 /* DatadogCore.h in Headers */, + D2CB6E0D27C50EAE00A62B57 /* ObjcAppLaunchHandler.h in Headers */, + D2CB6E0E27C50EAE00A62B57 /* ObjcExceptionHandler.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6F9527C5217A00A62B57 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CB6F9627C5217A00A62B57 /* DatadogObjc.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FBD27C5348200A62B57 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CB6FBE27C5348200A62B57 /* DatadogCrashReporting.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2356298D57AA00C6C7E6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3CE119FD29F7BE0000202522 /* DatadogWebViewTracking iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3CE11A1929F7BE0B00202522 /* Build configuration list for PBXNativeTarget "DatadogWebViewTracking iOS" */; + buildPhases = ( + 3CE119F929F7BE0000202522 /* Headers */, + 3CE119FA29F7BE0000202522 /* Sources */, + 3CE119FB29F7BE0000202522 /* Frameworks */, + 3CE119FC29F7BE0000202522 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3C9C6BB729F7C0C000581C43 /* PBXTargetDependency */, + ); + name = "DatadogWebViewTracking iOS"; + productName = "DatadogWebViewTracking iOS"; + productReference = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; + productType = "com.apple.product-type.framework"; + }; + 3CE11A0429F7BE0300202522 /* DatadogWebViewTrackingTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3CE11A1A29F7BE0B00202522 /* Build configuration list for PBXNativeTarget "DatadogWebViewTrackingTests iOS" */; + buildPhases = ( + 3CE11A0129F7BE0300202522 /* Sources */, + 3CE11A0229F7BE0300202522 /* Frameworks */, + 3CE11A0329F7BE0300202522 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3C41694029FBF5F20042B9D2 /* PBXTargetDependency */, + 3C41693E29FBF5BB0042B9D2 /* PBXTargetDependency */, + 3CE11A0829F7BE0500202522 /* PBXTargetDependency */, + ); + name = "DatadogWebViewTrackingTests iOS"; + productName = "DatadogWebViewTracking iOSTests"; + productReference = 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 61133B81242393DE00786299 /* DatadogCore iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61133B96242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogCore iOS" */; + buildPhases = ( + 61133B7D242393DE00786299 /* Headers */, + 61133B7E242393DE00786299 /* Sources */, + 61133B80242393DE00786299 /* Resources */, + 61133C772423A4C300786299 /* ⚙️ Run linter */, + 61569793256CF6C300C6AADA /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D2303A07298D5317001A1FA3 /* PBXTargetDependency */, + D2C1A52C29C4C92800946C31 /* PBXTargetDependency */, + ); + name = "DatadogCore iOS"; + productName = Datadog; + productReference = 61133B82242393DE00786299 /* DatadogCore.framework */; + productType = "com.apple.product-type.framework"; + }; + 61133B8A242393DE00786299 /* DatadogCoreTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61133B99242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogCoreTests iOS" */; + buildPhases = ( + 61133B87242393DE00786299 /* Sources */, + 61133B88242393DE00786299 /* Frameworks */, + 61133B89242393DE00786299 /* Resources */, + 9EA6A53C24489AB100621535 /* ⚙️ Run linter */, + ); + buildRules = ( + ); + dependencies = ( + 61441C5A24619A08003D8BB8 /* PBXTargetDependency */, + ); + name = "DatadogCoreTests iOS"; + productName = DatadogTests; + productReference = 61133B8B242393DE00786299 /* DatadogCoreTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 61133BEF242397DA00786299 /* DatadogObjc iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61133C01242397DA00786299 /* Build configuration list for PBXNativeTarget "DatadogObjc iOS" */; + buildPhases = ( + 61133BEB242397DA00786299 /* Headers */, + 61133BEC242397DA00786299 /* Sources */, + 61133BED242397DA00786299 /* Frameworks */, + 61133BEE242397DA00786299 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 61133C732423993200786299 /* PBXTargetDependency */, + 61A2CC292A4449210000FF25 /* PBXTargetDependency */, + D206BB882A41CA6800F43BA2 /* PBXTargetDependency */, + D2A434A52A8E3F900028E329 /* PBXTargetDependency */, + ); + name = "DatadogObjc iOS"; + productName = DatadogObjc; + productReference = 61133BF0242397DA00786299 /* DatadogObjc.framework */; + productType = "com.apple.product-type.framework"; + }; + 6133D1E52A6ED9E100384BEF /* DatadogSessionReplay iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6133D1F12A6ED9E100384BEF /* Build configuration list for PBXNativeTarget "DatadogSessionReplay iOS" */; + buildPhases = ( + 6133D1E82A6ED9E100384BEF /* Headers */, + 6133D1E92A6ED9E100384BEF /* Sources */, + 6133D1EE2A6ED9E100384BEF /* Frameworks */, + 6133D1F02A6ED9E100384BEF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6133D1E62A6ED9E100384BEF /* PBXTargetDependency */, + ); + name = "DatadogSessionReplay iOS"; + productName = "DatadogWebViewTracking iOS"; + productReference = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; + productType = "com.apple.product-type.framework"; + }; + 6133D1F62A6EDB7700384BEF /* DatadogSessionReplayTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6133D2042A6EDB7700384BEF /* Build configuration list for PBXNativeTarget "DatadogSessionReplayTests iOS" */; + buildPhases = ( + 6133D1FD2A6EDB7700384BEF /* Sources */, + 6133D2002A6EDB7700384BEF /* Frameworks */, + 6133D2032A6EDB7700384BEF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6133D1F72A6EDB7700384BEF /* PBXTargetDependency */, + 6133D1F92A6EDB7700384BEF /* PBXTargetDependency */, + 6133D20A2A6EDBAE00384BEF /* PBXTargetDependency */, + 6158155B2AB4534F002C60D7 /* PBXTargetDependency */, + ); + name = "DatadogSessionReplayTests iOS"; + productName = "DatadogWebViewTracking iOSTests"; + productReference = 6133D2082A6EDB7700384BEF /* DatadogSessionReplayTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 61441C0124616DE9003D8BB8 /* Example iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example iOS" */; + buildPhases = ( + 61441BFE24616DE9003D8BB8 /* Sources */, + 61441BFF24616DE9003D8BB8 /* Frameworks */, + 61441C0024616DE9003D8BB8 /* Resources */, + D240687A27CF982B00C04F44 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D231F7B62A00FF9A000D6239 /* PBXTargetDependency */, + D231F7B42A00FF8F000D6239 /* PBXTargetDependency */, + ); + name = "Example iOS"; + packageProductDependencies = ( + ); + productName = Example; + productReference = 61441C0224616DE9003D8BB8 /* Example iOS.app */; + productType = "com.apple.product-type.application"; + }; + 618F983F265BC486009959F8 /* E2EInstrumentationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 618F9847265BC486009959F8 /* Build configuration list for PBXNativeTarget "E2EInstrumentationTests" */; + buildPhases = ( + 618F983C265BC486009959F8 /* Sources */, + 618F983D265BC486009959F8 /* Frameworks */, + 618F983E265BC486009959F8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 618F9846265BC486009959F8 /* PBXTargetDependency */, + ); + name = E2EInstrumentationTests; + productName = E2EInstrumentationTests; + productReference = 618F9840265BC486009959F8 /* E2EInstrumentationTests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 6199362A265BA958009D7EA8 /* E2E */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6199363F265BA95A009D7EA8 /* Build configuration list for PBXNativeTarget "E2E" */; + buildPhases = ( + 61993627265BA958009D7EA8 /* Sources */, + 61993628265BA958009D7EA8 /* Frameworks */, + 61993629265BA958009D7EA8 /* Resources */, + 61993660265BB6A6009D7EA8 /* ⚙️ Embed Framework Dependencies */, + 61BACC00267279CB00AB58DC /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 61993659265BB6A6009D7EA8 /* PBXTargetDependency */, + 6199365D265BB6A6009D7EA8 /* PBXTargetDependency */, + ); + name = E2E; + productName = E2E; + productReference = 6199362B265BA958009D7EA8 /* E2E.app */; + productType = "com.apple.product-type.application"; + }; + 61993664265BBEDC009D7EA8 /* E2ETests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6199366C265BBEDC009D7EA8 /* Build configuration list for PBXNativeTarget "E2ETests" */; + buildPhases = ( + 61993661265BBEDC009D7EA8 /* Sources */, + 61993662265BBEDC009D7EA8 /* Frameworks */, + 61993663265BBEDC009D7EA8 /* Resources */, + 61993671265BBF8E009D7EA8 /* ⚙️ Run linter */, + ); + buildRules = ( + ); + dependencies = ( + 6199366B265BBEDC009D7EA8 /* PBXTargetDependency */, + ); + name = E2ETests; + productName = E2ETests; + productReference = 61993665265BBEDC009D7EA8 /* E2ETests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 61B7885325C180CB002675B5 /* DatadogCrashReporting iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61B7886B25C180CB002675B5 /* Build configuration list for PBXNativeTarget "DatadogCrashReporting iOS" */; + buildPhases = ( + 61B7884F25C180CB002675B5 /* Headers */, + 61B7885025C180CB002675B5 /* Sources */, + 61B7885225C180CB002675B5 /* Resources */, + 6170DC2325C18762003AED5C /* ⚙️ Run linter */, + 61B7885125C180CB002675B5 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D231F7B02A00FF28000D6239 /* PBXTargetDependency */, + ); + name = "DatadogCrashReporting iOS"; + productName = DatadogCrashReporting; + productReference = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; + productType = "com.apple.product-type.framework"; + }; + 61B7885B25C180CB002675B5 /* DatadogCrashReportingTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61B7886C25C180CB002675B5 /* Build configuration list for PBXNativeTarget "DatadogCrashReportingTests iOS" */; + buildPhases = ( + 61B7885825C180CB002675B5 /* Sources */, + 61B7885925C180CB002675B5 /* Frameworks */, + 61B7885A25C180CB002675B5 /* Resources */, + 6170DC2425C18784003AED5C /* ⚙️ Run linter */, + ); + buildRules = ( + ); + dependencies = ( + 61B7885F25C180CB002675B5 /* PBXTargetDependency */, + ); + name = "DatadogCrashReportingTests iOS"; + productName = DatadogCrashReportingTests; + productReference = 61B7885C25C180CB002675B5 /* DatadogCrashReportingTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D207317B29A5226A00ECBF94 /* DatadogLogs iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D207319129A5226B00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogs iOS" */; + buildPhases = ( + D207317729A5226A00ECBF94 /* Headers */, + D207317829A5226A00ECBF94 /* Sources */, + D207317929A5226A00ECBF94 /* Frameworks */, + D207317A29A5226A00ECBF94 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D207319A29A5232A00ECBF94 /* PBXTargetDependency */, + ); + name = "DatadogLogs iOS"; + productName = DatadogLogs; + productReference = D207317C29A5226A00ECBF94 /* DatadogLogs.framework */; + productType = "com.apple.product-type.framework"; + }; + D207318229A5226A00ECBF94 /* DatadogLogsTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D207319229A5226B00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogsTests iOS" */; + buildPhases = ( + D207317F29A5226A00ECBF94 /* Sources */, + D207318029A5226A00ECBF94 /* Frameworks */, + D207318129A5226A00ECBF94 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D207318629A5226B00ECBF94 /* PBXTargetDependency */, + ); + name = "DatadogLogsTests iOS"; + productName = DatadogLogsTests; + productReference = D207318329A5226A00ECBF94 /* DatadogLogsTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D20731A429A5279D00ECBF94 /* DatadogLogs tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D20731B029A5279D00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogs tvOS" */; + buildPhases = ( + D20731A729A5279D00ECBF94 /* Headers */, + D20731A829A5279D00ECBF94 /* Sources */, + D20731AD29A5279D00ECBF94 /* Frameworks */, + D20731AF29A5279D00ECBF94 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2A783E329A53414003B03BB /* PBXTargetDependency */, + ); + name = "DatadogLogs tvOS"; + productName = DatadogLogs; + productReference = D20731B429A5279D00ECBF94 /* DatadogLogs.framework */; + productType = "com.apple.product-type.framework"; + }; + D23039A4298D513C001A1FA3 /* DatadogInternal iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D23039AC298D513D001A1FA3 /* Build configuration list for PBXNativeTarget "DatadogInternal iOS" */; + buildPhases = ( + D23039A0298D513C001A1FA3 /* Headers */, + D23039A1298D513C001A1FA3 /* Sources */, + D23039A2298D513C001A1FA3 /* Frameworks */, + D23039A3298D513C001A1FA3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "DatadogInternal iOS"; + productName = DatadogInternal; + productReference = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; + productType = "com.apple.product-type.framework"; + }; + D23F8E4D29DDCD28001CFAE8 /* DatadogRUM tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D23F8E9529DDCD28001CFAE8 /* Build configuration list for PBXNativeTarget "DatadogRUM tvOS" */; + buildPhases = ( + D23F8E5029DDCD28001CFAE8 /* Headers */, + D23F8E5129DDCD28001CFAE8 /* Sources */, + D23F8E9029DDCD28001CFAE8 /* Frameworks */, + D23F8E9229DDCD28001CFAE8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D23F8ED029DDCD5C001CFAE8 /* PBXTargetDependency */, + ); + name = "DatadogRUM tvOS"; + productName = DatadogRUM; + productReference = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; + productType = "com.apple.product-type.framework"; + }; + D23F8E9A29DDCD38001CFAE8 /* DatadogRUMTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D23F8EC929DDCD38001CFAE8 /* Build configuration list for PBXNativeTarget "DatadogRUMTests tvOS" */; + buildPhases = ( + D23F8E9F29DDCD38001CFAE8 /* Sources */, + D23F8EC529DDCD38001CFAE8 /* Frameworks */, + D23F8EC829DDCD38001CFAE8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D22A031D29F7DABE002C02C6 /* PBXTargetDependency */, + ); + name = "DatadogRUMTests tvOS"; + productName = DatadogRUMTests; + productReference = D23F8ECD29DDCD38001CFAE8 /* DatadogRUMTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D24067F827CE6C9E00C04F44 /* Example tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D240684927CE6C9E00C04F44 /* Build configuration list for PBXNativeTarget "Example tvOS" */; + buildPhases = ( + D24067FD27CE6C9E00C04F44 /* Sources */, + D240682F27CE6C9E00C04F44 /* Frameworks */, + D240683327CE6C9E00C04F44 /* Resources */, + D240684527CE6C9E00C04F44 /* ⚙️ Embed Framework Dependencies */, + ); + buildRules = ( + ); + dependencies = ( + D240685727CF5D0100C04F44 /* PBXTargetDependency */, + D231F7B82A00FFA3000D6239 /* PBXTargetDependency */, + ); + name = "Example tvOS"; + packageProductDependencies = ( + ); + productName = Example; + productReference = D240684D27CE6C9E00C04F44 /* Example tvOS.app */; + productType = "com.apple.product-type.application"; + }; + D257953D298ABA65008A1BE5 /* TestUtilities iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2579542298ABA65008A1BE5 /* Build configuration list for PBXNativeTarget "TestUtilities iOS" */; + buildPhases = ( + D2579539298ABA65008A1BE5 /* Headers */, + D257953A298ABA65008A1BE5 /* Sources */, + D257953B298ABA65008A1BE5 /* Frameworks */, + D257953C298ABA65008A1BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3C41694229FBF6100042B9D2 /* PBXTargetDependency */, + ); + name = "TestUtilities iOS"; + packageProductDependencies = ( + 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */, + ); + productName = TestUtilities; + productReference = D257953E298ABA65008A1BE5 /* TestUtilities.framework */; + productType = "com.apple.product-type.framework"; + }; + D2579571298ABB83008A1BE5 /* TestUtilities tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2579587298ABB83008A1BE5 /* Build configuration list for PBXNativeTarget "TestUtilities tvOS" */; + buildPhases = ( + D2579576298ABB83008A1BE5 /* Headers */, + D2579577298ABB83008A1BE5 /* Sources */, + D2579582298ABB83008A1BE5 /* Frameworks */, + D2579584298ABB83008A1BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D26F741529ACBDAD00D25622 /* PBXTargetDependency */, + ); + name = "TestUtilities tvOS"; + packageProductDependencies = ( + 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */, + ); + productName = TestUtilities; + productReference = D257958B298ABB83008A1BE5 /* TestUtilities.framework */; + productType = "com.apple.product-type.framework"; + }; + D25EE93329C4C3C300CE3839 /* DatadogTrace iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D25EE94929C4C3C400CE3839 /* Build configuration list for PBXNativeTarget "DatadogTrace iOS" */; + buildPhases = ( + D25EE92F29C4C3C300CE3839 /* Headers */, + D25EE93029C4C3C300CE3839 /* Sources */, + D25EE93129C4C3C300CE3839 /* Frameworks */, + D25EE93229C4C3C300CE3839 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2C1A51129C4C4EF00946C31 /* PBXTargetDependency */, + ); + name = "DatadogTrace iOS"; + packageProductDependencies = ( + ); + productName = DatadogTrace; + productReference = D25EE93429C4C3C300CE3839 /* DatadogTrace.framework */; + productType = "com.apple.product-type.framework"; + }; + D25EE93A29C4C3C300CE3839 /* DatadogTraceTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D25EE94A29C4C3C400CE3839 /* Build configuration list for PBXNativeTarget "DatadogTraceTests iOS" */; + buildPhases = ( + D25EE93729C4C3C300CE3839 /* Sources */, + D25EE93829C4C3C300CE3839 /* Frameworks */, + D25EE93929C4C3C300CE3839 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D25EE93E29C4C3C300CE3839 /* PBXTargetDependency */, + ); + name = "DatadogTraceTests iOS"; + packageProductDependencies = ( + ); + productName = DatadogTraceTests; + productReference = D25EE93B29C4C3C300CE3839 /* DatadogTraceTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D29A9F3329DD84AA005C54A4 /* DatadogRUM iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D29A9F4929DD84AB005C54A4 /* Build configuration list for PBXNativeTarget "DatadogRUM iOS" */; + buildPhases = ( + D29A9F2F29DD84AA005C54A4 /* Headers */, + D29A9F3029DD84AA005C54A4 /* Sources */, + D29A9F3129DD84AA005C54A4 /* Frameworks */, + D29A9F3229DD84AA005C54A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D29A9F4E29DD8525005C54A4 /* PBXTargetDependency */, + ); + name = "DatadogRUM iOS"; + productName = DatadogRUM; + productReference = D29A9F3429DD84AA005C54A4 /* DatadogRUM.framework */; + productType = "com.apple.product-type.framework"; + }; + D29A9F3A29DD84AB005C54A4 /* DatadogRUMTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D29A9F4A29DD84AB005C54A4 /* Build configuration list for PBXNativeTarget "DatadogRUMTests iOS" */; + buildPhases = ( + D29A9F3729DD84AB005C54A4 /* Sources */, + D29A9F3829DD84AB005C54A4 /* Frameworks */, + D29A9F3929DD84AB005C54A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D29A9F3E29DD84AB005C54A4 /* PBXTargetDependency */, + ); + name = "DatadogRUMTests iOS"; + productName = DatadogRUMTests; + productReference = D29A9F3B29DD84AB005C54A4 /* DatadogRUMTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2A783EE29A534F9003B03BB /* DatadogLogsTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2A783FD29A534F9003B03BB /* Build configuration list for PBXNativeTarget "DatadogLogsTests tvOS" */; + buildPhases = ( + D2A783F129A534F9003B03BB /* Sources */, + D2A783FA29A534F9003B03BB /* Frameworks */, + D2A783FC29A534F9003B03BB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D22A031B29F7DAA9002C02C6 /* PBXTargetDependency */, + ); + name = "DatadogLogsTests tvOS"; + productName = DatadogLogsTests; + productReference = D2A7840129A534F9003B03BB /* DatadogLogsTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2C1A53329C4F2DF00946C31 /* DatadogTrace tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2C1A55629C4F2DF00946C31 /* Build configuration list for PBXNativeTarget "DatadogTrace tvOS" */; + buildPhases = ( + D2C1A53629C4F2DF00946C31 /* Headers */, + D2C1A53729C4F2DF00946C31 /* Sources */, + D2C1A55329C4F2DF00946C31 /* Frameworks */, + D2C1A55529C4F2DF00946C31 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2C1A57729C4F30000946C31 /* PBXTargetDependency */, + ); + name = "DatadogTrace tvOS"; + productName = DatadogTrace; + productReference = D2C1A55A29C4F2DF00946C31 /* DatadogTrace.framework */; + productType = "com.apple.product-type.framework"; + }; + D2C1A55B29C4F2E800946C31 /* DatadogTraceTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2C1A56F29C4F2E800946C31 /* Build configuration list for PBXNativeTarget "DatadogTraceTests tvOS" */; + buildPhases = ( + D2C1A55E29C4F2E800946C31 /* Sources */, + D2C1A56B29C4F2E800946C31 /* Frameworks */, + D2C1A56E29C4F2E800946C31 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D25CFA9B29C4F41F00E3A43D /* PBXTargetDependency */, + ); + name = "DatadogTraceTests tvOS"; + productName = DatadogTraceTests; + productReference = D2C1A57329C4F2E800946C31 /* DatadogTraceTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2CB6E0A27C50EAE00A62B57 /* DatadogCore tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2CB6ECD27C50EAE00A62B57 /* Build configuration list for PBXNativeTarget "DatadogCore tvOS" */; + buildPhases = ( + D2CB6E0B27C50EAE00A62B57 /* Headers */, + D2CB6E0F27C50EAE00A62B57 /* Sources */, + D2CB6ECA27C50EAE00A62B57 /* Resources */, + D2CB6ECB27C50EAE00A62B57 /* ⚙️ Run linter */, + D2CB6ECC27C50EAE00A62B57 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3C4D5FF12A0115CB00F1FF78 /* PBXTargetDependency */, + 3C4D5FEF2A0115C600F1FF78 /* PBXTargetDependency */, + ); + name = "DatadogCore tvOS"; + productName = Datadog; + productReference = D2CB6ED127C50EAE00A62B57 /* DatadogCore.framework */; + productType = "com.apple.product-type.framework"; + }; + D2CB6ED327C520D400A62B57 /* DatadogCoreTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2CB6F8B27C520D400A62B57 /* Build configuration list for PBXNativeTarget "DatadogCoreTests tvOS" */; + buildPhases = ( + D2CB6ED627C520D400A62B57 /* Sources */, + D2CB6F8627C520D400A62B57 /* Frameworks */, + D2CB6F8927C520D400A62B57 /* Resources */, + D2CB6F8A27C520D400A62B57 /* ⚙️ Run linter */, + ); + buildRules = ( + ); + dependencies = ( + D240686D27CF687200C04F44 /* PBXTargetDependency */, + ); + name = "DatadogCoreTests tvOS"; + productName = DatadogTests; + productReference = D2CB6F8F27C520D400A62B57 /* DatadogCoreTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2CB6F9227C5217A00A62B57 /* DatadogObjc tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2CB6FAC27C5217A00A62B57 /* Build configuration list for PBXNativeTarget "DatadogObjc tvOS" */; + buildPhases = ( + D2CB6F9527C5217A00A62B57 /* Headers */, + D2CB6F9727C5217A00A62B57 /* Sources */, + D2CB6FA927C5217A00A62B57 /* Frameworks */, + D2CB6FAB27C5217A00A62B57 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2CB6FB627C5234300A62B57 /* PBXTargetDependency */, + 61A2CC2E2A4449300000FF25 /* PBXTargetDependency */, + D206BB8D2A41CA7000F43BA2 /* PBXTargetDependency */, + ); + name = "DatadogObjc tvOS"; + productName = DatadogObjc; + productReference = D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */; + productType = "com.apple.product-type.framework"; + }; + D2CB6FBA27C5348200A62B57 /* DatadogCrashReporting tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2CB6FCD27C5348200A62B57 /* Build configuration list for PBXNativeTarget "DatadogCrashReporting tvOS" */; + buildPhases = ( + D2CB6FBD27C5348200A62B57 /* Headers */, + D2CB6FBF27C5348200A62B57 /* Sources */, + D2CB6FC827C5348200A62B57 /* Resources */, + D2CB6FC927C5348200A62B57 /* ⚙️ Run linter */, + D2CB6FCA27C5348200A62B57 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + D231F7B22A00FF2F000D6239 /* PBXTargetDependency */, + ); + name = "DatadogCrashReporting tvOS"; + productName = DatadogCrashReporting; + productReference = D2CB6FD127C5348200A62B57 /* DatadogCrashReporting.framework */; + productType = "com.apple.product-type.framework"; + }; + D2CB6FD327C5352300A62B57 /* DatadogCrashReportingTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2CB6FE827C5352300A62B57 /* Build configuration list for PBXNativeTarget "DatadogCrashReportingTests tvOS" */; + buildPhases = ( + D2CB6FD827C5352300A62B57 /* Sources */, + D2CB6FE327C5352300A62B57 /* Frameworks */, + D2CB6FE627C5352300A62B57 /* Resources */, + D2CB6FE727C5352300A62B57 /* ⚙️ Run linter */, + ); + buildRules = ( + ); + dependencies = ( + D28D5D5427C53A60008E72D0 /* PBXTargetDependency */, + ); + name = "DatadogCrashReportingTests tvOS"; + productName = DatadogCrashReportingTests; + productReference = D2CB6FEC27C5352300A62B57 /* DatadogCrashReportingTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DA2381298D57AA00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternal tvOS" */; + buildPhases = ( + D2DA2356298D57AA00C6C7E6 /* Headers */, + D2DA2357298D57AA00C6C7E6 /* Sources */, + D2DA237F298D57AA00C6C7E6 /* Frameworks */, + D2DA2380298D57AA00C6C7E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "DatadogInternal tvOS"; + productName = DatadogInternal; + productReference = D2DA2385298D57AA00C6C7E6 /* DatadogInternal.framework */; + productType = "com.apple.product-type.framework"; + }; + D2DA2389298D588800C6C7E6 /* DatadogInternalTests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DA2391298D588A00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternalTests iOS" */; + buildPhases = ( + D2DA2386298D588800C6C7E6 /* Sources */, + D2DA2387298D588800C6C7E6 /* Frameworks */, + D2DA2388298D588800C6C7E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2DA2390298D588A00C6C7E6 /* PBXTargetDependency */, + ); + name = "DatadogInternalTests iOS"; + productName = DatadogInternalTests; + productReference = D2DA238A298D588800C6C7E6 /* DatadogInternalTests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D2DA23AD298D59DC00C6C7E6 /* DatadogInternalTests tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2DA23BF298D59DC00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternalTests tvOS" */; + buildPhases = ( + D2DA23B0298D59DC00C6C7E6 /* Sources */, + D2DA23BB298D59DC00C6C7E6 /* Frameworks */, + D2DA23BE298D59DC00C6C7E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2DA23D3298D620F00C6C7E6 /* PBXTargetDependency */, + ); + name = "DatadogInternalTests tvOS"; + productName = DatadogInternalTests; + productReference = D2DA23C3298D59DC00C6C7E6 /* DatadogInternalTests tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 61133B79242393DE00786299 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1410; + ORGANIZATIONNAME = Datadog; + TargetAttributes = { + 3CE119FD29F7BE0000202522 = { + CreatedOnToolsVersion = 14.0.1; + }; + 3CE11A0429F7BE0300202522 = { + CreatedOnToolsVersion = 14.0.1; + LastSwiftMigration = 1430; + }; + 61133B81242393DE00786299 = { + CreatedOnToolsVersion = 11.3.1; + }; + 61133B8A242393DE00786299 = { + CreatedOnToolsVersion = 11.3.1; + LastSwiftMigration = 1200; + TestTargetID = 61441C0124616DE9003D8BB8; + }; + 61133BEF242397DA00786299 = { + CreatedOnToolsVersion = 11.3.1; + }; + 6133D1E52A6ED9E100384BEF = { + LastSwiftMigration = 1430; + }; + 6133D1F62A6EDB7700384BEF = { + LastSwiftMigration = 1520; + TestTargetID = 61441C0124616DE9003D8BB8; + }; + 61441C0124616DE9003D8BB8 = { + CreatedOnToolsVersion = 11.4; + LastSwiftMigration = 1200; + }; + 618F983F265BC486009959F8 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 6199362A265BA958009D7EA8; + }; + 6199362A265BA958009D7EA8 = { + CreatedOnToolsVersion = 12.5; + }; + 61993664265BBEDC009D7EA8 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 6199362A265BA958009D7EA8; + }; + 61B7885325C180CB002675B5 = { + CreatedOnToolsVersion = 12.3; + LastSwiftMigration = 1230; + }; + 61B7885B25C180CB002675B5 = { + CreatedOnToolsVersion = 12.3; + }; + D207317B29A5226A00ECBF94 = { + CreatedOnToolsVersion = 14.2; + }; + D207318229A5226A00ECBF94 = { + CreatedOnToolsVersion = 14.2; + }; + D23039A4298D513C001A1FA3 = { + CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1410; + }; + D257953D298ABA65008A1BE5 = { + CreatedOnToolsVersion = 14.2; + }; + D25EE93329C4C3C300CE3839 = { + CreatedOnToolsVersion = 14.2; + }; + D25EE93A29C4C3C300CE3839 = { + CreatedOnToolsVersion = 14.2; + }; + D29A9F3329DD84AA005C54A4 = { + CreatedOnToolsVersion = 14.2; + }; + D29A9F3A29DD84AB005C54A4 = { + CreatedOnToolsVersion = 14.2; + }; + D2CB6ED327C520D400A62B57 = { + TestTargetID = D24067F827CE6C9E00C04F44; + }; + D2DA2389298D588800C6C7E6 = { + CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1420; + }; + }; + }; + buildConfigurationList = 61133B7C242393DE00786299 /* Build configuration list for PBXProject "Datadog" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 61133B78242393DE00786299; + packageReferences = ( + 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */, + ); + productRefGroup = 61133B83242393DE00786299 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D23039A4298D513C001A1FA3 /* DatadogInternal iOS */, + D2DA2389298D588800C6C7E6 /* DatadogInternalTests iOS */, + D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */, + D2DA23AD298D59DC00C6C7E6 /* DatadogInternalTests tvOS */, + D207317B29A5226A00ECBF94 /* DatadogLogs iOS */, + D207318229A5226A00ECBF94 /* DatadogLogsTests iOS */, + D20731A429A5279D00ECBF94 /* DatadogLogs tvOS */, + D2A783EE29A534F9003B03BB /* DatadogLogsTests tvOS */, + D25EE93329C4C3C300CE3839 /* DatadogTrace iOS */, + D25EE93A29C4C3C300CE3839 /* DatadogTraceTests iOS */, + D2C1A53329C4F2DF00946C31 /* DatadogTrace tvOS */, + D2C1A55B29C4F2E800946C31 /* DatadogTraceTests tvOS */, + D29A9F3329DD84AA005C54A4 /* DatadogRUM iOS */, + D29A9F3A29DD84AB005C54A4 /* DatadogRUMTests iOS */, + D23F8E4D29DDCD28001CFAE8 /* DatadogRUM tvOS */, + D23F8E9A29DDCD38001CFAE8 /* DatadogRUMTests tvOS */, + 61B7885325C180CB002675B5 /* DatadogCrashReporting iOS */, + 61B7885B25C180CB002675B5 /* DatadogCrashReportingTests iOS */, + D2CB6FBA27C5348200A62B57 /* DatadogCrashReporting tvOS */, + D2CB6FD327C5352300A62B57 /* DatadogCrashReportingTests tvOS */, + 61133B81242393DE00786299 /* DatadogCore iOS */, + 61133BEF242397DA00786299 /* DatadogObjc iOS */, + 61133B8A242393DE00786299 /* DatadogCoreTests iOS */, + D2CB6E0A27C50EAE00A62B57 /* DatadogCore tvOS */, + D2CB6F9227C5217A00A62B57 /* DatadogObjc tvOS */, + D2CB6ED327C520D400A62B57 /* DatadogCoreTests tvOS */, + 61441C0124616DE9003D8BB8 /* Example iOS */, + D24067F827CE6C9E00C04F44 /* Example tvOS */, + 6199362A265BA958009D7EA8 /* E2E */, + 61993664265BBEDC009D7EA8 /* E2ETests */, + 618F983F265BC486009959F8 /* E2EInstrumentationTests */, + D257953D298ABA65008A1BE5 /* TestUtilities iOS */, + D2579571298ABB83008A1BE5 /* TestUtilities tvOS */, + 3CE119FD29F7BE0000202522 /* DatadogWebViewTracking iOS */, + 3CE11A0429F7BE0300202522 /* DatadogWebViewTrackingTests iOS */, + 6133D1E52A6ED9E100384BEF /* DatadogSessionReplay iOS */, + 6133D1F62A6EDB7700384BEF /* DatadogSessionReplayTests iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3CE119FC29F7BE0000202522 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3CE11A0329F7BE0300202522 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133B80242393DE00786299 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D28FCC352B5EBAAF00CCC077 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133B89242393DE00786299 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133BEE242397DA00786299 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6133D1F02A6ED9E100384BEF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6133D2032A6EDB7700384BEF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 960B26C02D0360EE00D7196F /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61441C0024616DE9003D8BB8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D240688627CFA64A00C04F44 /* LaunchScreen.storyboard in Resources */, + 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */, + 61441C0C24616DE9003D8BB8 /* Main iOS.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 618F983E265BC486009959F8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61993629265BA958009D7EA8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6199363A265BA95A009D7EA8 /* LaunchScreen.storyboard in Resources */, + 61993637265BA95A009D7EA8 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61993663265BBEDC009D7EA8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885225C180CB002675B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7A9022BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885A25C180CB002675B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207317A29A5226A00ECBF94 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207318129A5226A00ECBF94 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D20731AF29A5279D00ECBF94 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23039A3298D513C001A1FA3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8E9229DDCD28001CFAE8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7A9002BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8EC829DDCD38001CFAE8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D240683327CE6C9E00C04F44 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D240683D27CE6C9E00C04F44 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D257953C298ABA65008A1BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2579584298ABB83008A1BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93229C4C3C300CE3839 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93929C4C3C300CE3839 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3229DD84AA005C54A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7A8FF2BA1C24A00F46845 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3929DD84AB005C54A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2A783FC29A534F9003B03BB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A55529C4F2DF00946C31 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A56E29C4F2E800946C31 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6ECA27C50EAE00A62B57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D28FCC362B5FCBD100CCC077 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6F8927C520D400A62B57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FAB27C5217A00A62B57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FC827C5348200A62B57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2A7A9032BA1C4B100F46845 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FE627C5352300A62B57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2380298D57AA00C6C7E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2388298D588800C6C7E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA23BE298D59DC00C6C7E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 61133C772423A4C300786299 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif [ \"$CARTHAGE\" == \"YES\" ]; then\n echo \"Skipping linting for carthage build...\"\nelif which swiftlint > /dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + 6170DC2325C18762003AED5C /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif [ \"$CARTHAGE\" == \"YES\" ]; then\n echo \"Skipping linting for carthage build...\"\nelif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + 6170DC2425C18784003AED5C /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + 61993671265BBF8E009D7EA8 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + 61BACC00267279CB00AB58DC /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd ${SOURCE_ROOT}/..\n./tools/nightly-e2e-tests/nightly_e2e.py lint --tests-dir ../../Datadog/E2ETests\n"; + showEnvVarsInLog = 0; + }; + 9EA6A53C24489AB100621535 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + D2CB6ECB27C50EAE00A62B57 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif [ \"$CARTHAGE\" == \"YES\" ]; then\n echo \"Skipping linting for carthage build...\"\nelif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + D2CB6F8A27C520D400A62B57 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + D2CB6FC927C5348200A62B57 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif [ \"$CARTHAGE\" == \"YES\" ]; then\n echo \"Skipping linting for carthage build...\"\nelif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; + D2CB6FE727C5352300A62B57 /* ⚙️ Run linter */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "⚙️ Run linter"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3CE119FA29F7BE0000202522 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C85D42129F7C5C900AFF894 /* WebViewTracking.swift in Sources */, + D297324B2A5C108700827599 /* MessageEmitter.swift in Sources */, + 6174D6042BFB9AB600EC7469 /* WebViewTracking+objc.swift in Sources */, + D29732492A5C108700827599 /* DDScriptMessageHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3CE11A0129F7BE0300202522 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D29732532A5C109A00827599 /* WebViewTrackingTests.swift in Sources */, + D29732512A5C109A00827599 /* MessageEmitterTests.swift in Sources */, + D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133B7E242393DE00786299 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61DA8CA928609C5B0074A606 /* Directories.swift in Sources */, + 6128F56E2BA223A100D35B08 /* FeatureDataStore.swift in Sources */, + D2EFA868286DA85700F1FAA6 /* DatadogContextProvider.swift in Sources */, + D26C49BF288982DA00802B2D /* FeatureUpload.swift in Sources */, + A70A82652A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */, + 61D3E0D2277B23F1008BE766 /* KronosInternetAddress.swift in Sources */, + D2553826288F0B1A00727FAD /* BatteryStatusPublisher.swift in Sources */, + 61D3E0D5277B23F1008BE766 /* KronosNTPPacket.swift in Sources */, + 6128F5712BA223D100D35B08 /* DataStore+TLV.swift in Sources */, + 61133BCF2423979B00786299 /* FileWriter.swift in Sources */, + 6179FFD3254ADB1700556A0B /* ObjcAppLaunchHandler.m in Sources */, + 3C0D5DE42A543E3400446CF9 /* EventGenerator.swift in Sources */, + D2303A0A298D5412001A1FA3 /* AsyncWriter.swift in Sources */, + D29CDD3228211A2200F7DAA5 /* TLVBlock.swift in Sources */, + 6128F5742BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, + D2553829288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */, + D224430629E95C2C00274EC7 /* MessageBus.swift in Sources */, + 61F930BE2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, + 6128F5772BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, + 6139CD712589FAFD007E8BB7 /* Retrying.swift in Sources */, + D29A9F9029DD876F005C54A4 /* CITestIntegration.swift in Sources */, + 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, + D2FB125D292FBB56005B13F8 /* Datadog+Internal.swift in Sources */, + D2A7840F29A53B2F003B03BB /* Directory.swift in Sources */, + 61D3E0DB277B23F1008BE766 /* KronosNSTimer+ClosureKit.swift in Sources */, + D20605A92874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */, + 614396722A67D74F00197326 /* BatchMetrics.swift in Sources */, + D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */, + 613E792F2577B0F900DFCC17 /* Reader.swift in Sources */, + 61D3E0D3277B23F1008BE766 /* KronosDNSResolver.swift in Sources */, + D2DC4BF627F484AA00E4FB96 /* DataEncryption.swift in Sources */, + D2FB1254292E0E96005B13F8 /* TrackingConsentPublisher.swift in Sources */, + 61D3E0D6277B23F1008BE766 /* KronosClock.swift in Sources */, + D2A7841129A53B2F003B03BB /* File.swift in Sources */, + D286626E2A43487500852CE3 /* Datadog.swift in Sources */, + 61F930C22BA1C41A005F0EE2 /* TLVBlockReader.swift in Sources */, + 613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */, + D2B3F04D282A85FD00C2B5EE /* DatadogCore.swift in Sources */, + 61133BD62423979B00786299 /* DataUploader.swift in Sources */, + 617699182A860D9D0030022B /* HTTPClient.swift in Sources */, + D21C26C528A3B49C005DD405 /* FeatureStorage.swift in Sources */, + 61133BD42423979B00786299 /* FileReader.swift in Sources */, + D29294E0291D5ED100F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, + 61D3E0D9277B23F1008BE766 /* KronosNTPProtocol.swift in Sources */, + 61D3E0DA277B23F1008BE766 /* KronosTimeFreeze.swift in Sources */, + 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */, + 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, + 61D3E0D4277B23F1008BE766 /* KronosTimeStorage.swift in Sources */, + D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */, + 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, + D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */, + 61DA8CAF28620C760074A606 /* Cryptography.swift in Sources */, + E1D5AEA724B4D45B007F194B /* Versioning.swift in Sources */, + 61133BD82423979B00786299 /* URLSessionClient.swift in Sources */, + 61D3E0D8277B23F1008BE766 /* KronosNTPClient.swift in Sources */, + D20605B22874E1660047275C /* CarrierInfoPublisher.swift in Sources */, + D2A1EE32287DA51900D28DFB /* UserInfoPublisher.swift in Sources */, + 61133BD52423979B00786299 /* DataUploadConditions.swift in Sources */, + 61D3E0D7277B23F1008BE766 /* KronosData+Bytes.swift in Sources */, + 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, + 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, + D2A1EE23287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133B87242393DE00786299 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D29A9FDA29DDC6D0005C54A4 /* RUMEventFileOutputTests.swift in Sources */, + D28F836829C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */, + 61B8BA91281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, + 6174D6062BFB9D6400EC7469 /* DDWebViewTracking+apiTests.m in Sources */, + 614798962A459AA80095CB02 /* DDTraceTests.swift in Sources */, + D25085102976E30000E931C3 /* DatadogRemoteFeatureMock.swift in Sources */, + 6167E6DD2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, + 612C13D02AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */, + 61A1A44929643254007909E7 /* DatadogCoreProxy.swift in Sources */, + D2A1EE3B287EECC000D28DFB /* CarrierInfoPublisherTests.swift in Sources */, + D22743D829DEB8B4001A7EF9 /* VitalInfoTests.swift in Sources */, + D2C5D5302B84F71200B63F36 /* WebRecordIntegrationTests.swift in Sources */, + 61FF282824B8A31E000B3D9B /* RUMEventMatcher.swift in Sources */, + D29A9FD829DDC686005C54A4 /* UIKitRUMViewsPredicateTests.swift in Sources */, + D29294E3291D652C00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */, + 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */, + 61EF78C1257F842000EDCCB3 /* FeatureTests.swift in Sources */, + 61133C5D2423990D00786299 /* DataUploadConditionsTests.swift in Sources */, + 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */, + 61133C5A2423990D00786299 /* FileTests.swift in Sources */, + 61133C6B2423990D00786299 /* LogMatcher.swift in Sources */, + 61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */, + D2EFA875286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */, + 61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */, + A727C4BB2BADB3AB00707DFD /* DDSessionReplay+apiTests.m in Sources */, + 613F9C182BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift in Sources */, + 6128F5842BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift in Sources */, + D22743DA29DEB8B4001A7EF9 /* VitalMemoryReaderTests.swift in Sources */, + 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */, + 61133C582423990D00786299 /* FileWriterTests.swift in Sources */, + D22743DC29DEB8B4001A7EF9 /* VitalRefreshRateReaderTests.swift in Sources */, + 6176991E2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift in Sources */, + 617B954224BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift in Sources */, + 61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */, + 6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */, + 3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */, + 6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */, + D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, + 6147989C2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */, + D22743EB29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */, + 61DA8CB2286215DE0074A606 /* CryptographyTests.swift in Sources */, + 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */, + 6128F58A2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */, + 61A2CC212A443D330000FF25 /* DDRUMConfigurationTests.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 */, + 61E8C5082B28898800E709B4 /* StartingRUMSessionTests.swift in Sources */, + 616B668E259CC28E00968EE8 /* DDRUMMonitorTests.swift in Sources */, + 9EE5AD8226205B82001E699E /* DDNSURLSessionDelegateTests.swift in Sources */, + 61133C4A2423990D00786299 /* DDConfigurationTests.swift in Sources */, + 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */, + 6184751526EFCF1300C7C9C5 /* DatadogTestsObserver.swift in Sources */, + D22743D929DEB8B4001A7EF9 /* VitalInfoSamplerTests.swift in Sources */, + 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */, + 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 */, + 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */, + D2A1EE36287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift in Sources */, + 61BBD19724ED50040023E65F /* DatadogConfigurationTests.swift in Sources */, + 6176991B2A86121B0030022B /* HTTPClientMock.swift in Sources */, + 61133C612423990D00786299 /* URLSessionClientTests.swift in Sources */, + 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, + D22743DB29DEB8B4001A7EF9 /* VitalCPUReaderTests.swift in Sources */, + 61DA8CAC2861C3720074A606 /* DirectoriesTests.swift in Sources */, + 612C13D62AAB35EB0086B5D1 /* SRSegmentMatcher.swift in Sources */, + 61133C5E2423990D00786299 /* DataUploadDelayTests.swift in Sources */, + 61133C5C2423990D00786299 /* DataUploadWorkerTests.swift in Sources */, + A728ADB02934EB0900397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */, + 61FC5F3525CC1898006BB4DE /* CrashContextProviderTests.swift in Sources */, + 49274906288048B500ECD49B /* InternalProxyTests.swift in Sources */, + D25CFA9F29C860E100E3A43D /* TracingFeatureMocks.swift in Sources */, + 9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */, + 61133C6E2423990D00786299 /* DatadogExtensions.swift in Sources */, + 61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */, + 61133C592423990D00786299 /* FilesOrchestratorTests.swift in Sources */, + 61A763DC252DB2B3005A23F2 /* NSURLSessionBridge.m in Sources */, + D2A1EE442886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */, + D29A9FD029DDC58E005C54A4 /* RUMFeatureTests.swift in Sources */, + D2552AF52BBC492400A45725 /* WebEventIntegrationTests.swift in Sources */, + 61F930C82BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */, + 3C1890152ABDE9BF00CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, + D28F836529C9E69E00EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */, + 61133C4B2423990D00786299 /* DDLogsTests.swift in Sources */, + 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */, + D22743E929DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */, + 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */, + 61F2723F25C86DA400D54BF8 /* CrashReportingFeatureMocks.swift in Sources */, + 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */, + 61112F8E2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */, + A7DA18072AB0CA5E00F76337 /* DDUIKitRUMActionsPredicateTests.swift in Sources */, + 6139CD772589FEE3007E8BB7 /* RetryingTests.swift in Sources */, + D29A9FC429DDB710005C54A4 /* RUMInternalProxyTests.swift in Sources */, + 61133C482423990D00786299 /* DDDatadogTests.swift in Sources */, + D2B3F0442823EE8400C2B5EE /* TLVBlockTests.swift in Sources */, + 6128F57B2BA35D6200D35B08 /* FeatureDataStoreTests.swift in Sources */, + D29A9FCE29DDC4BA005C54A4 /* RUMFeatureMocks.swift in Sources */, + 61133C5B2423990D00786299 /* DirectoryTests.swift in Sources */, + 610ABD4C2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */, + A79B0F64292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */, + D2B3F052282E827700C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, + D20605B92875729E0047275C /* ContextValuePublisherMock.swift in Sources */, + D24C9C4D29A7BA3F002057CF /* LogsMocks.swift in Sources */, + A7CA21832BEBB2E200732571 /* ExtensionBackgroundTaskCoordinatorTests.swift in Sources */, + 61B5E42B26DFC433000B0A5F /* DDNSURLSessionDelegate+apiTests.m in Sources */, + 6167E7062B82A9FD00C3CA2D /* GeneratingBacktraceTests.swift in Sources */, + A7CA21802BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */, + D20605C52875895E0047275C /* KronosClockMock.swift in Sources */, + 61133C642423990D00786299 /* LoggerTests.swift in Sources */, + 6134CDB12A691E850061CCD9 /* BatchMetricsTests.swift in Sources */, + D24C9C6029A7CB0A002057CF /* DatadogLogsFeatureTests.swift in Sources */, + 617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */, + D244B3A3271EDACD003E1B29 /* SwiftUIExtensionsTests.swift in Sources */, + D24C9C6429A7CB7B002057CF /* CrashLogReceiverTests.swift in Sources */, + 61B5E42126DF85C7000B0A5F /* DDRUMMonitor+apiTests.m in Sources */, + 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */, + 3CF673362B4807490016CE17 /* OTelSpanTests.swift in Sources */, + 61F3E36D2BC7D66700C7881E /* HeadBasedSamplingTests.swift in Sources */, + D20FD9D32ACC08D1004D3569 /* WebKitMocks.swift in Sources */, + 618353BC2A69470A0085F84A /* CoreMetricsIntegrationTests.swift in Sources */, + 61133C4D2423990D00786299 /* CoreTelephonyMocks.swift in Sources */, + D2C7E3AB28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift in Sources */, + D20605B6287572640047275C /* DatadogContextProviderMock.swift in Sources */, + 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */, + 61DA8CB828647A500074A606 /* InternalLoggerTests.swift in Sources */, + D2FB1257292E0F0E005B13F8 /* TrackingConsentPublisherTests.swift in Sources */, + 614B78F1296D7B63009C6B92 /* LowPowerModePublisherTests.swift in Sources */, + 61F2724925C943C500D54BF8 /* CrashReporterTests.swift in Sources */, + 61F930C52BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, + 6172472725D673D7007085B3 /* CrashContextTests.swift in Sources */, + 61A2CC242A44454D0000FF25 /* DDRUMTests.swift in Sources */, + D25CFAA329C8644E00E3A43D /* Casting+Tracing.swift in Sources */, + 61BAD46A26415FCE001886CA /* OTSpanTests.swift in Sources */, + 61B5E42726DFB145000B0A5F /* DDDatadog+apiTests.m in Sources */, + A7DA18042AB0C91200F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */, + 6121627C247D220500AC5D67 /* TracingWithLoggingIntegrationTests.swift in Sources */, + D2A1EE3E2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */, + 61B5E42926DFB60A000B0A5F /* DDConfiguration+apiTests.m in Sources */, + D22743E429DEB933001A7EF9 /* UIViewControllerSwizzlerTests.swift in Sources */, + 3CCCA5C72ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */, + 6184751826EFD03400C7C9C5 /* DatadogTestsObserverLoader.m in Sources */, + D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */, + 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */, + 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61133BEC242397DA00786299 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */, + 6111C58225C0081F00F5C4A2 /* RUMDataModels+objc.swift in Sources */, + 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */, + 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */, + 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 */, + 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */, + 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, + 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */, + 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */, + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, + 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */, + 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */, + 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */, + F6E106542C75E0D000716DC6 /* LogsDataModels+objc.swift in Sources */, + 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6133D1E92A6ED9E100384BEF /* Sources */ = { + 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 */, + 61054E8D2A6EE10A00AAA894 /* RUMContextReceiver.swift in Sources */, + 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 */, + 61054E6C2A6EE10A00AAA894 /* SystemColors.swift in Sources */, + 61054E812A6EE10A00AAA894 /* UIStepperRecorder.swift in Sources */, + 61054E632A6EE10A00AAA894 /* SessionReplayConfiguration.swift in Sources */, + 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */, + D2C5D52B2B84F6AB00B63F36 /* WebViewRecordReceiver.swift in Sources */, + 61054E652A6EE10A00AAA894 /* AppWindowObserver.swift in Sources */, + A74A72812B0CEE4900771FEB /* ResourceRequestBuilder.swift in Sources */, + 61054E7B2A6EE10A00AAA894 /* UIViewRecorder.swift in Sources */, + 61054E762A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift in Sources */, + 61054E802A6EE10A00AAA894 /* UIPickerViewRecorder.swift in Sources */, + 61054E612A6EE10A00AAA894 /* SRCompression.swift in Sources */, + 61054E8F2A6EE10A00AAA894 /* SegmentRequestBuilder.swift in Sources */, + 61054E8B2A6EE10A00AAA894 /* SessionReplayFeature.swift in Sources */, + 61054E992A6EE10A00AAA894 /* WireframesBuilder.swift in Sources */, + 61054E892A6EE10A00AAA894 /* NodeIDGenerator.swift in Sources */, + 61054E962A6EE10A00AAA894 /* Diff+SRWireframes.swift in Sources */, + D22C5BD02A98A6660024CC1F /* Baggages.swift in Sources */, + 61054E902A6EE10A00AAA894 /* SegmentJSON.swift in Sources */, + 61054E672A6EE10A00AAA894 /* Recorder.swift in Sources */, + 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */, + 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */, + 61054E982A6EE10A00AAA894 /* RecordsBuilder.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 */, + 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 */, + 61054E662A6EE10A00AAA894 /* KeyWindowObserver.swift in Sources */, + 61054E972A6EE10A00AAA894 /* Diff.swift in Sources */, + 61054E6E2A6EE10A00AAA894 /* RecordingCoordinator.swift in Sources */, + 61054E9F2A6EE10B00AAA894 /* Errors.swift in Sources */, + 61054E642A6EE10A00AAA894 /* SessionReplay.swift in Sources */, + 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+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 */, + 61054E8A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift in Sources */, + 61054E7A2A6EE10A00AAA894 /* UIImageViewRecorder.swift in Sources */, + A7B932FC2B1F6A0A00AE6477 /* SRDataModels.swift in Sources */, + A795069C2B974C8200AC4814 /* SessionReplay+objc.swift in Sources */, + 61054E752A6EE10A00AAA894 /* ViewTreeSnapshot.swift in Sources */, + 61054EA02A6EE10B00AAA894 /* Colors.swift in Sources */, + 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6133D1FD2A6EDB7700384BEF /* Sources */ = { + 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 */, + 61054FCE2A6EE1BA00AAA894 /* RUMContextObserverMock.swift in Sources */, + D2C5D52D2B84F6D800B63F36 /* WebViewRecordReceiverTests.swift in Sources */, + 61054FBA2A6EE1BA00AAA894 /* UIImageViewRecorderTests.swift in Sources */, + 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 /* 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 */, + 61054FBE2A6EE1BA00AAA894 /* UIImageViewWireframesBuilderTests.swift in Sources */, + 61054FBC2A6EE1BA00AAA894 /* UIStepperRecorderTests.swift in Sources */, + 61054FCB2A6EE1BA00AAA894 /* QueueMocks.swift in Sources */, + 61054FC42A6EE1BA00AAA894 /* RecorderTests.swift in Sources */, + 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+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 */, + 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 */, + 61054FCF2A6EE1BA00AAA894 /* RUMContextReceiverTests.swift in Sources */, + 61054FC92A6EE1BA00AAA894 /* RecorderMocks.swift in Sources */, + 61054FBB2A6EE1BA00AAA894 /* UISwitchRecorderTests.swift in Sources */, + A7F651302B7655DE004B0EDB /* UIImageResourceTests.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 /* 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 */, + 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */, + 3C33E4072BEE35A8003B2988 /* RUMContextMocks.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61441BFE24616DE9003D8BB8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, + 61020C2A2757AD91005EEAEA /* BackgroundLocationMonitor.swift in Sources */, + 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */, + 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 */, + D2F44FC2299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift in Sources */, + 61441C992461A649003D8BB8 /* DebugLoggingViewController.swift in Sources */, + 617699212A8A7DF50030022B /* DebugManualTraceInjectionViewController.swift in Sources */, + 617247AF25DA9BEA007085B3 /* CrashReportingObjcHelpers.m in Sources */, + 61776CED273BEA5500F93802 /* DebugRUMSessionViewController.swift in Sources */, + 61776D4E273E6D9F00F93802 /* SwiftUI.swift in Sources */, + 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */, + 614CADD72510BAC000B93D2D /* Environment.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 618F983C265BC486009959F8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 618F9843265BC486009959F8 /* E2EInstrumentationTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61993627265BA958009D7EA8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 618F984E265BC905009959F8 /* E2EConfig.swift in Sources */, + 6199362E265BA959009D7EA8 /* E2EAppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61993661265BBEDC009D7EA8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9E5B6D30270C85AB002499B8 /* RUMUtils.swift in Sources */, + 6167C7952666622800D4CF07 /* LoggingE2EHelpers.swift in Sources */, + 618F984F265BC905009959F8 /* E2EConfig.swift in Sources */, + 61216B7B2667A9AE0089DCD1 /* LogsConfigurationE2ETests.swift in Sources */, + 9E5B6D32270DE9E5002499B8 /* RUMTrackingConsentE2ETests.swift in Sources */, + 6187A53926FCBE240015D94A /* TracerE2ETests.swift in Sources */, + 61993668265BBEDC009D7EA8 /* E2ETests.swift in Sources */, + 61216B852667CFFE0089DCD1 /* RUME2EHelpers.swift in Sources */, + 9E5B6D2E270C84B4002499B8 /* RUMMonitorE2ETests.swift in Sources */, + 61216B762666DDA10089DCD1 /* LoggerConfigurationTests.swift in Sources */, + 61B3BD52266128D300A9BEF0 /* LoggerE2ETests.swift in Sources */, + 9E64849D27031071007CCD7B /* RUMGlobalE2ETests.swift in Sources */, + 61216B842667CFF70089DCD1 /* DatadogE2EHelpers.swift in Sources */, + 6167C79326665D6900D4CF07 /* E2EUtils.swift in Sources */, + 6147E3B3270486920092BC9F /* TraceConfigurationE2ETests.swift in Sources */, + 6185F4AE26FE1956001A7641 /* SpanE2ETests.swift in Sources */, + 61216B802667C79B0089DCD1 /* LogsTrackingConsentE2ETests.swift in Sources */, + 61D3E0EA277E0C58008BE766 /* KronosE2ETests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885025C180CB002675B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D214DA8929DF2D6A004D0AE8 /* CrashReportSender.swift in Sources */, + D293302C2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */, + 617247B825DAB0E2007085B3 /* DDCrashReportBuilder.swift in Sources */, + 612556BB268DD9BF002BCE74 /* DDCrashReportExporter.swift in Sources */, + 61FDBA1326971953001D9D43 /* CrashReportMinifier.swift in Sources */, + D214DA8329DF2D5E004D0AE8 /* CrashReporting.swift in Sources */, + 61F2728B25C9561A00D54BF8 /* PLCrashReporterIntegration.swift in Sources */, + D214DA8129DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */, + 6167E7032B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */, + D214DA8A29DF2D6A004D0AE8 /* CrashContext.swift in Sources */, + 612556B0268C8D31002BCE74 /* CrashReport.swift in Sources */, + D214DA8B29DF2D6A004D0AE8 /* CrashContextProvider.swift in Sources */, + 615CC40C2694A56D0005F08C /* SwiftExtensions.swift in Sources */, + 6170DC1C25C18729003AED5C /* PLCrashReporterPlugin.swift in Sources */, + 61F2727425C9509D00D54BF8 /* ThirdPartyCrashReporter.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61B7885825C180CB002675B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61FDBA1726974CA9001D9D43 /* DDCrashReportBuilderTests.swift in Sources */, + 615CC4102694A64D0005F08C /* SwiftExtensionTests.swift in Sources */, + D243BBC0276C9D640019C857 /* PLCrashReporterIntegrationTests.swift in Sources */, + 61FDBA15269722B4001D9D43 /* CrashReportMinifierTests.swift in Sources */, + 61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */, + 61B7886225C180CB002675B5 /* CrashReportingPluginTests.swift in Sources */, + 615CC4132695957C0005F08C /* CrashReportTests.swift in Sources */, + 61F272B125C95ED800D54BF8 /* Mocks.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D207317829A5226A00ECBF94 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D24C9C3F29A79772002057CF /* Logger.swift in Sources */, + D2D30E5B2A40BF540020C553 /* Logs.swift in Sources */, + D20731C329A528EB00ECBF94 /* LogEventMapper.swift in Sources */, + D243BBF229A6209C000B9CEC /* RequestBuilder.swift in Sources */, + D2B249942A4598FE00DD4F9F /* LoggerProtocol+Internal.swift in Sources */, + D20731C229A528EB00ECBF94 /* LogEventEncoder.swift in Sources */, + D243BBEC29A614CE000B9CEC /* LoggerProtocol.swift in Sources */, + D207319629A522F600ECBF94 /* ConsoleLogger.swift in Sources */, + D20731C529A528EC00ECBF94 /* LogEventSanitizer.swift in Sources */, + 49D8C0BD2AC5F2BB0075E427 /* Logs+Internal.swift in Sources */, + 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; + }; + D207317F29A5226A00ECBF94 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + 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 */, + D20FD9CF2AC6FF42004D3569 /* WebViewLogReceiverTests.swift in Sources */, + D2A783ED29A534F2003B03BB /* LoggingFeatureMocks.swift in Sources */, + D2B249972A45E10500DD4F9F /* LoggerTests.swift in Sources */, + D2A783EA29A53468003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C12C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D20731A829A5279D00ECBF94 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D24C9C4029A79772002057CF /* Logger.swift in Sources */, + D2D30E5C2A40BF540020C553 /* Logs.swift in Sources */, + D20731C829A528ED00ECBF94 /* LogEventMapper.swift in Sources */, + D243BBF329A6209C000B9CEC /* RequestBuilder.swift in Sources */, + D2B249952A4598FE00DD4F9F /* LoggerProtocol+Internal.swift in Sources */, + D20731C729A528ED00ECBF94 /* LogEventEncoder.swift in Sources */, + D243BBED29A614CE000B9CEC /* LoggerProtocol.swift in Sources */, + D20731A929A5279D00ECBF94 /* ConsoleLogger.swift in Sources */, + D20731CA29A528ED00ECBF94 /* LogEventSanitizer.swift in Sources */, + 49D8C0BE2AC5F2BC0075E427 /* Logs+Internal.swift in Sources */, + 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; + }; + D23039A1298D513C001A1FA3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 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 */, + D2EBEE2629BA160F00B15732 /* B3HTTPHeaders.swift in Sources */, + D23354FC2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 619F5CEC2BF5089C004BFE70 /* GlobalRUMAttributes.swift in Sources */, + D23039F3298D5236001A1FA3 /* DynamicCodingKey.swift in Sources */, + D2BEEDB22B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */, + D23039FE298D5236001A1FA3 /* FeatureRequestBuilder.swift in Sources */, + D2160CE429C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */, + D2160CC929C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */, + D2EBEE2929BA160F00B15732 /* HTTPHeadersWriter.swift in Sources */, + D2432CF929EDB22C00D93657 /* Flushable.swift in Sources */, + D23039F7298D5236001A1FA3 /* AttributesSanitizer.swift in Sources */, + D23039EB298D5236001A1FA3 /* DatadogFeature.swift in Sources */, + D2BEEDBA2B33638F0065F3AC /* NetworkInstrumentationSwizzler.swift in Sources */, + 3CBDE6742AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */, + D23039E4298D5236001A1FA3 /* CarrierInfo.swift in Sources */, + D2303A03298D5236001A1FA3 /* DDError.swift in Sources */, + D23039F4298D5236001A1FA3 /* AnyCodable.swift in Sources */, + D29A9F9529DDB1DB005C54A4 /* UIKitExtensions.swift in Sources */, + 6167E6E82B8122E900C3CA2D /* BacktraceReport.swift in Sources */, + D2BEEDB52B3360820065F3AC /* URLSessionSwizzler.swift in Sources */, + D2EBEE2529BA160F00B15732 /* TraceID.swift in Sources */, + D2EBEE2129BA160F00B15732 /* W3CHTTPHeaders.swift in Sources */, + 6167E6F62B81E94C00C3CA2D /* DDThread.swift in Sources */, + D2BEEDAC2B3356710065F3AC /* URLSessionTaskSwizzler.swift in Sources */, + D23039E3298D5236001A1FA3 /* BatteryStatus.swift in Sources */, + D2EBEE2A29BA160F00B15732 /* TracingHTTPHeaders.swift in Sources */, + 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 */, + D23039FC298D5236001A1FA3 /* DataFormat.swift in Sources */, + D2160CED29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */, + D2160C9E29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, + D23039F5298D5236001A1FA3 /* AnyEncodable.swift in Sources */, + D2303A00298D5236001A1FA3 /* DatadogExtended.swift in Sources */, + D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */, + 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, + 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 */, + D23039F8298D5236001A1FA3 /* InternalLogger.swift in Sources */, + 6174D6132BFDF16C00EC7469 /* BundleType.swift in Sources */, + D2303A01298D5236001A1FA3 /* DateFormatting.swift in Sources */, + 3C9B27252B9F174700569C07 /* SpanID.swift in Sources */, + D2216EC02A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, + D23039F1298D5236001A1FA3 /* AnyDecodable.swift in Sources */, + 6167E6E22B81207200C3CA2D /* DDCrashReport.swift in Sources */, + D2160CC529C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, + 6167E6FD2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */, + D23039DD298D5235001A1FA3 /* DD.swift in Sources */, + D2160C9A29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */, + D2EBEE2229BA160F00B15732 /* TracePropagationHeadersReader.swift in Sources */, + D2303A02298D5236001A1FA3 /* ReadWriteLock.swift in Sources */, + D2EBEE2429BA160F00B15732 /* W3CHTTPHeadersReader.swift in Sources */, + A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */, + 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 */, + D23039FB298D5236001A1FA3 /* URLRequestBuilder.swift in Sources */, + 3CA8525F2BF2073800B52CBA /* TraceContextInjection.swift in Sources */, + D23039F6298D5236001A1FA3 /* Attributes.swift in Sources */, + D20731CB29A52E6000ECBF94 /* Sampler.swift in Sources */, + D2EBEE2029BA160F00B15732 /* TracePropagationHeadersWriter.swift in Sources */, + D23039F2298D5236001A1FA3 /* AnyDecoder.swift in Sources */, + D23039EF298D5236001A1FA3 /* FeatureMessage.swift in Sources */, + 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 */, + D2EA0F462C0E1AE300CB20F8 /* SessionReplayConfiguration.swift in Sources */, + 6167E6F92B81E95900C3CA2D /* BinaryImage.swift in Sources */, + 6174D60C2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */, + D23039F0298D5236001A1FA3 /* AnyEncoder.swift in Sources */, + D2A783D429A5309F003B03BB /* SwiftExtensions.swift in Sources */, + 3C0D5DD72A543B3B00446CF9 /* Event.swift in Sources */, + 3CBDE68A2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */, + D270CDDD2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */, + D22F06D929DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8E5129DDCD28001CFAE8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6167E6D42B7F8B3300C3CA2D /* AppHangsMonitor.swift in Sources */, + D23F8E5229DDCD28001CFAE8 /* UIViewControllerHandler.swift in Sources */, + D23F8E5329DDCD28001CFAE8 /* RUMCommand.swift in Sources */, + D23F8E5429DDCD28001CFAE8 /* ValuePublisher.swift in Sources */, + D23F8E5529DDCD28001CFAE8 /* RUMEventSanitizer.swift in Sources */, + D23F8E5729DDCD28001CFAE8 /* RUMScopeDependencies.swift in Sources */, + D23F8E5829DDCD28001CFAE8 /* VitalMemoryReader.swift in Sources */, + 6194B9342BB451DB00179430 /* FatalAppHangsHandler.swift in Sources */, + D23F8E5929DDCD28001CFAE8 /* WebViewEventReceiver.swift in Sources */, + 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 */, + D23F8E6029DDCD28001CFAE8 /* PerformanceMetric.swift in Sources */, + D23F8E6129DDCD28001CFAE8 /* RUMConfiguration.swift in Sources */, + 61F930CC2BA213AC005F0EE2 /* AppHang.swift in Sources */, + 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 */, + D23F8E6929DDCD28001CFAE8 /* RUMContextAttributes.swift in Sources */, + D23F8E6B29DDCD28001CFAE8 /* RUMMonitor.swift in Sources */, + D23F8E6C29DDCD28001CFAE8 /* RUMContextProvider.swift in Sources */, + 61DA6F6D2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */, + D23F8E6D29DDCD28001CFAE8 /* ViewIdentifier.swift in Sources */, + 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 */, + D23F8E7129DDCD28001CFAE8 /* RUMEventBuilder.swift in Sources */, + D23F8E7229DDCD28001CFAE8 /* ErrorMessageReceiver.swift in Sources */, + D23F8E7329DDCD28001CFAE8 /* SwiftUIActionModifier.swift in Sources */, + D23F8E7429DDCD28001CFAE8 /* RUMCommandSubscriber.swift in Sources */, + 6194B92B2BB4116A00179430 /* RUMDataStore.swift in Sources */, + 6194B9312BB451C100179430 /* NonFatalAppHangsHandler.swift in Sources */, + D23F8E7529DDCD28001CFAE8 /* RUMUserActionScope.swift in Sources */, + 6194B92E2BB43F9C00179430 /* FatalErrorContextNotifier.swift in Sources */, + 6167E6D72B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift in Sources */, + 61C713A42A3B78F900FA735A /* RUMMonitorProtocol.swift in Sources */, + 6174D6112BFDEA4600EC7469 /* SessionEndedMetric.swift in Sources */, + 3C0D5DED2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */, + D23F8E7629DDCD28001CFAE8 /* RUMConnectivityInfoProvider.swift in Sources */, + D23F8E7729DDCD28001CFAE8 /* UIKitRUMViewsPredicate.swift in Sources */, + 61C713A62A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift in Sources */, + D23F8E7829DDCD28001CFAE8 /* LongTaskObserver.swift in Sources */, + D23F8E7A29DDCD28001CFAE8 /* SessionReplayDependency.swift in Sources */, + 616F8C282BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */, + D23F8E7B29DDCD28001CFAE8 /* RUMDeviceInfo.swift in Sources */, + D23F8E7C29DDCD28001CFAE8 /* RUMOffViewEventsHandlingRule.swift in Sources */, + D23F8E7D29DDCD28001CFAE8 /* RUMScope.swift in Sources */, + D23F8E7E29DDCD28001CFAE8 /* CrashReportReceiver.swift in Sources */, + D23F8E7F29DDCD28001CFAE8 /* UIViewControllerSwizzler.swift in Sources */, + D23F8E8029DDCD28001CFAE8 /* VitalInfoSampler.swift in Sources */, + D23F8E8129DDCD28001CFAE8 /* RUMViewScope.swift in Sources */, + 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 */, + D23F8E8929DDCD28001CFAE8 /* RUMOperatingSystemInfo.swift in Sources */, + D23F8E8A29DDCD28001CFAE8 /* RUMEventsMapper.swift in Sources */, + D23F8E8B29DDCD28001CFAE8 /* RUMContext.swift in Sources */, + D23F8E8C29DDCD28001CFAE8 /* RUMBaggageKeys.swift in Sources */, + 6174D6212C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, + D23F8E8D29DDCD28001CFAE8 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8C2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, + D23F8E8E29DDCD28001CFAE8 /* UIEventCommandFactory.swift in Sources */, + D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */, + 61DCC84F2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D23F8E9F29DDCD38001CFAE8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6188697D2A4376F700E8996B /* RUMConfigurationTests.swift in Sources */, + 61DCC8482C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */, + 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 */, + 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 */, + 61C713BD2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */, + D23F8EB329DDCD38001CFAE8 /* ErrorMessageReceiverTests.swift in Sources */, + 61C713C12A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, + D23F8EB429DDCD38001CFAE8 /* RUMApplicationScopeTests.swift in Sources */, + D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */, + 61C713CB2A3DC22700FA735A /* RUMTests.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 */, + 6167E6DB2B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift in Sources */, + 3C0D5DEA2A543EA300446CF9 /* RUMViewEventsFilterTests.swift in Sources */, + D23F8EC429DDCD38001CFAE8 /* RUMCommandTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D24067FD27CE6C9E00C04F44 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */, + D2F44FC3299BD5600074B0D9 /* UIViewController+KeyboardControlling.swift in Sources */, + D240680827CE6C9E00C04F44 /* ConsoleOutputInterceptor.swift in Sources */, + D240681E27CE6C9E00C04F44 /* ExampleAppDelegate.swift in Sources */, + D240682B27CE6C9E00C04F44 /* UIButton+Disabling.swift in Sources */, + D240682D27CE6C9E00C04F44 /* Environment.swift in Sources */, + D240686827CF642900C04F44 /* SwiftUI.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D257953A298ABA65008A1BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61C713D32A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, + D2160CF229C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, + D257955B298ABB04008A1BE5 /* XCTestCase.swift in Sources */, + 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 */, + 6167E7202B837FB200C3CA2D /* DDThreadMocks.swift in Sources */, + D2EBEE4829BA17C400B15732 /* NetworkInstrumentationMocks.swift in Sources */, + 3C0D5DEF2A5442A900446CF9 /* EventMocks.swift in Sources */, + D24C9C5529A7C5F3002057CF /* DateProvider.swift in Sources */, + D2579559298ABB04008A1BE5 /* DDAssert.swift in Sources */, + D2579552298ABB04008A1BE5 /* FileWriterMock.swift in Sources */, + 6167E72C2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */, + 6167E7252B837FF100C3CA2D /* BinaryImageMocks.swift in Sources */, + 61AE74172AD7DA9B008DB9BB /* FeatureMessageMocks.swift in Sources */, + 6167E71B2B837F7A00C3CA2D /* BacktraceReportMocks.swift in Sources */, + D2A7840329A536AD003B03BB /* PrintFunctionMock.swift in Sources */, + D2A7840D29A53A4B003B03BB /* TestsDirectory.swift in Sources */, + D2DA23CF298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */, + 6174D61D2C007B3300EC7469 /* ModuleName.swift in Sources */, + D2160CF029C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */, + D2579554298ABB04008A1BE5 /* FeatureBaggageMock.swift in Sources */, + 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 */, + 6167E7292B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */, + D2DA23C7298D5AC000C6C7E6 /* TelemetryMocks.swift in Sources */, + 613F9C1B2BB03188007C7606 /* FeatureScopeMock.swift in Sources */, + D2DA23CA298D5C1300C6C7E6 /* UIKitMocks.swift in Sources */, + 6188900F2AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */, + 61C713D02A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */, + D2579555298ABB04008A1BE5 /* PassthroughCoreMock.swift in Sources */, + D257955A298ABB04008A1BE5 /* SwiftExtensions.swift in Sources */, + 61AE74142AD6EF55008DB9BB /* JSONObjectMatcher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2579577298ABB83008A1BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61C713D42A3DFB4900FA735A /* FuzzyHelpers.swift in Sources */, + D2160CF329C0ED3C00FAA9A5 /* ServerMock.swift in Sources */, + D2579578298ABB83008A1BE5 /* XCTestCase.swift in Sources */, + 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 */, + 6167E7212B837FB200C3CA2D /* DDThreadMocks.swift in Sources */, + D2EBEE4929BA17C400B15732 /* NetworkInstrumentationMocks.swift in Sources */, + 3C0D5DF02A5442A900446CF9 /* EventMocks.swift in Sources */, + D24C9C5629A7C5F3002057CF /* DateProvider.swift in Sources */, + D257957C298ABB83008A1BE5 /* DDAssert.swift in Sources */, + D257957D298ABB83008A1BE5 /* FileWriterMock.swift in Sources */, + 6167E72D2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */, + 6167E7262B837FF100C3CA2D /* BinaryImageMocks.swift in Sources */, + 61AE74182AD7DA9B008DB9BB /* FeatureMessageMocks.swift in Sources */, + 6167E71C2B837F7A00C3CA2D /* BacktraceReportMocks.swift in Sources */, + D2A7840429A536AD003B03BB /* PrintFunctionMock.swift in Sources */, + D2A7840E29A53A4B003B03BB /* TestsDirectory.swift in Sources */, + D2DA23D0298D5F2300C6C7E6 /* FeatureMessageReceiverMock.swift in Sources */, + 6174D61E2C007B3300EC7469 /* ModuleName.swift in Sources */, + D2160CF129C0EC4D00FAA9A5 /* SingleFeatureCoreMock.swift in Sources */, + D257957E298ABB83008A1BE5 /* FeatureBaggageMock.swift in Sources */, + 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 */, + 6167E72A2B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */, + D2DA23C8298D5AC000C6C7E6 /* TelemetryMocks.swift in Sources */, + 613F9C1C2BB03188007C7606 /* FeatureScopeMock.swift in Sources */, + D2DA23CB298D5C1300C6C7E6 /* UIKitMocks.swift in Sources */, + 618890102AC58B8C00D0B966 /* TelemetryReceiverMock.swift in Sources */, + 61C713D12A3DEFF900FA735A /* FeatureRegistrationCoreMock.swift in Sources */, + D2579580298ABB83008A1BE5 /* PassthroughCoreMock.swift in Sources */, + D2579581298ABB83008A1BE5 /* SwiftExtensions.swift in Sources */, + 61AE74152AD6EF55008DB9BB /* JSONObjectMatcher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93029C4C3C300CE3839 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */, + 61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */, + D2C1A50229C4C4CB00946C31 /* Casting.swift in Sources */, + 3CC6AD182B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, + D2C1A50C29C4C4CB00946C31 /* DDNoOps.swift in Sources */, + D2C1A4FC29C4C4CB00946C31 /* RequestBuilder.swift in Sources */, + D2C1A50D29C4C4CB00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DD2B482E0400557951 /* NOPOTelSpan.swift in Sources */, + 61CE585A2B48174D00479510 /* SpanWriteContext.swift in Sources */, + D2C1A51829C4C53F00946C31 /* OTSpan.swift in Sources */, + D2C1A51429C4C53F00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEB2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FE92B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, + D2C1A51329C4C53F00946C31 /* OTReference.swift in Sources */, + 3C6C7FEF2B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, + D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */, + 3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */, + 61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, + 61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */, + D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, + D2C1A50929C4C4CB00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE72B459AAA006F5CBC /* OTelSpan.swift in Sources */, + D2C1A4FE29C4C4CB00946C31 /* SpanEventMapper.swift in Sources */, + D2C1A50329C4C4CB00946C31 /* DDFormat.swift in Sources */, + D2C1A51629C4C53F00946C31 /* OTConstants.swift in Sources */, + D2C1A50129C4C4CB00946C31 /* DDSpanContext.swift in Sources */, + D2C1A51529C4C53F00946C31 /* OTTracer.swift in Sources */, + D2C1A50429C4C4CB00946C31 /* (null) in Sources */, + D2C1A50729C4C4CB00946C31 /* DDSpan.swift in Sources */, + D2C1A50629C4C4CB00946C31 /* TracingWithLoggingIntegration.swift in Sources */, + D2C1A50B29C4C4CB00946C31 /* SpanEventBuilder.swift in Sources */, + D2C1A4FF29C4C4CB00946C31 /* Warnings.swift in Sources */, + D2C1A51729C4C53F00946C31 /* OTFormat.swift in Sources */, + D22C5BCB2A98A5400024CC1F /* Baggages.swift in Sources */, + 3CFF5D492B555F4F00FC483A /* OTelTracerProvider.swift in Sources */, + D2C1A4FA29C4C4CB00946C31 /* SpanSanitizer.swift in Sources */, + D2C1A50A29C4C4CB00946C31 /* TraceFeature.swift in Sources */, + D2C1A50829C4C4CB00946C31 /* TracingURLSessionHandler.swift in Sources */, + D2C1A50529C4C4CB00946C31 /* DatadogTracer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D25EE93729C4C3C300CE3839 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2C1A51E29C4C75700946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1D2B4F07FA00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, + D2C1A52429C4C75700946C31 /* TracingURLSessionHandlerTests.swift in Sources */, + 619CE75E2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, + D2C1A52329C4C75700946C31 /* WarningsTests.swift in Sources */, + D2C1A51D29C4C75700946C31 /* SpanEventBuilderTests.swift in Sources */, + 618C0FC02B482F6800266B38 /* SpanWriteContextTests.swift in Sources */, + D2C1A52229C4C75700946C31 /* DDNoopTracerTests.swift in Sources */, + 61F3E3632BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, + D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C7FFD2B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, + 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, + D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */, + 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */, + 3C3235A02B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */, + 3C6C7FFC2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, + D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */, + D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFB2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, + D2C1A51F29C4C75700946C31 /* ActiveSpansPoolTests.swift in Sources */, + D2C1A52529C4C75700946C31 /* SpanSanitizerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3029DD84AA005C54A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6167E6D32B7F8B3300C3CA2D /* AppHangsMonitor.swift in Sources */, + D29A9F8029DD85BB005C54A4 /* UIViewControllerHandler.swift in Sources */, + D29A9F5929DD85BB005C54A4 /* RUMCommand.swift in Sources */, + D29A9F8C29DD861C005C54A4 /* ValuePublisher.swift in Sources */, + D29A9F7F29DD85BB005C54A4 /* RUMEventSanitizer.swift in Sources */, + D29A9F5A29DD85BB005C54A4 /* RUMScopeDependencies.swift in Sources */, + D29A9F5B29DD85BB005C54A4 /* VitalMemoryReader.swift in Sources */, + 6194B9332BB451DB00179430 /* FatalAppHangsHandler.swift in Sources */, + D29A9F6229DD85BB005C54A4 /* WebViewEventReceiver.swift in Sources */, + 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 */, + D29A9F5529DD85BB005C54A4 /* PerformanceMetric.swift in Sources */, + D29A9F8129DD85BB005C54A4 /* RUMConfiguration.swift in Sources */, + 61F930CB2BA213AC005F0EE2 /* AppHang.swift in Sources */, + 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 */, + D29A9F6829DD85BB005C54A4 /* RUMContextAttributes.swift in Sources */, + D29A9F6329DD85BB005C54A4 /* RUMMonitor.swift in Sources */, + D29A9F7029DD85BB005C54A4 /* RUMContextProvider.swift in Sources */, + 61DA6F6C2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */, + D29A9F6029DD85BB005C54A4 /* ViewIdentifier.swift in Sources */, + 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 */, + D29A9F7C29DD85BB005C54A4 /* RUMEventBuilder.swift in Sources */, + D29A9F8829DD85BB005C54A4 /* ErrorMessageReceiver.swift in Sources */, + D29A9F8729DD85BB005C54A4 /* SwiftUIActionModifier.swift in Sources */, + D29A9F5D29DD85BB005C54A4 /* RUMCommandSubscriber.swift in Sources */, + 6194B92A2BB4116A00179430 /* RUMDataStore.swift in Sources */, + 6194B9302BB451C100179430 /* NonFatalAppHangsHandler.swift in Sources */, + D29A9F6529DD85BB005C54A4 /* RUMUserActionScope.swift in Sources */, + 6194B92D2BB43F9C00179430 /* FatalErrorContextNotifier.swift in Sources */, + 6167E6D62B7F8C3400C3CA2D /* AppHangsWatchdogThread.swift in Sources */, + 61C713A32A3B78F900FA735A /* RUMMonitorProtocol.swift in Sources */, + 6174D6102BFDEA4600EC7469 /* SessionEndedMetric.swift in Sources */, + 3C0D5DEC2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */, + D29A9F5829DD85BB005C54A4 /* RUMConnectivityInfoProvider.swift in Sources */, + D29A9F5E29DD85BB005C54A4 /* UIKitRUMViewsPredicate.swift in Sources */, + 61C713A52A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift in Sources */, + D29A9F5129DD85BB005C54A4 /* LongTaskObserver.swift in Sources */, + D29A9F8629DD85BB005C54A4 /* SessionReplayDependency.swift in Sources */, + 616F8C272BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */, + D29A9F7129DD85BB005C54A4 /* RUMDeviceInfo.swift in Sources */, + D29A9F5329DD85BB005C54A4 /* RUMOffViewEventsHandlingRule.swift in Sources */, + D29A9F5429DD85BB005C54A4 /* RUMScope.swift in Sources */, + D29A9F6129DD85BB005C54A4 /* CrashReportReceiver.swift in Sources */, + D29A9F7E29DD85BB005C54A4 /* UIViewControllerSwizzler.swift in Sources */, + D29A9F6B29DD85BB005C54A4 /* VitalInfoSampler.swift in Sources */, + D29A9F7529DD85BB005C54A4 /* RUMViewScope.swift in Sources */, + 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 */, + D29A9F6729DD85BB005C54A4 /* RUMOperatingSystemInfo.swift in Sources */, + D29A9F7D29DD85BB005C54A4 /* RUMEventsMapper.swift in Sources */, + D29A9F5029DD85BA005C54A4 /* RUMContext.swift in Sources */, + D29A9F8329DD85BB005C54A4 /* RUMBaggageKeys.swift in Sources */, + 6174D6202C009C6300EC7469 /* SessionEndedMetricController.swift in Sources */, + D29A9F8929DD85BB005C54A4 /* VitalRefreshRateReader.swift in Sources */, + 3CFF4F8B2C09E61A006F191D /* WatchdogTerminationAppState.swift in Sources */, + D29A9F6929DD85BB005C54A4 /* UIEventCommandFactory.swift in Sources */, + D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */, + 61DCC84E2C071DCD00CB59E5 /* TelemetryInterceptor.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D29A9F3729DD84AB005C54A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6188697C2A4376F700E8996B /* RUMConfigurationTests.swift in Sources */, + 61DCC8472C05CD0000CB59E5 /* SessionEndedMetricControllerTests.swift in Sources */, + 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 */, + 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 */, + 61C713BC2A3C95AD00FA735A /* RUMInstrumentationTests.swift in Sources */, + D29A9FBB29DDB483005C54A4 /* ErrorMessageReceiverTests.swift in Sources */, + 61C713C02A3C9DAD00FA735A /* RequestBuilderTests.swift in Sources */, + D29A9F9F29DDB483005C54A4 /* RUMApplicationScopeTests.swift in Sources */, + D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */, + 61C713CA2A3DC22700FA735A /* RUMTests.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 */, + 6167E6DA2B8004A500C3CA2D /* AppHangsWatchdogThreadTests.swift in Sources */, + 3C0D5DE92A543EA200446CF9 /* RUMViewEventsFilterTests.swift in Sources */, + D29A9FA729DDB483005C54A4 /* RUMCommandTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2A783F129A534F9003B03BB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + 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 */, + D20FD9D02AC6FF42004D3569 /* WebViewLogReceiverTests.swift in Sources */, + D2A783F629A534F9003B03BB /* LoggingFeatureMocks.swift in Sources */, + D2B249982A45E10500DD4F9F /* LoggerTests.swift in Sources */, + D2A783F729A534F9003B03BB /* LogMessageReceiverTests.swift in Sources */, + 615D52C22C88AB1E00F8B8FC /* SynchronizedAttributesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A53729C4F2DF00946C31 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */, + 61A2CC3D2A44BED30000FF25 /* Tracer.swift in Sources */, + D2C1A53829C4F2DF00946C31 /* Casting.swift in Sources */, + 3CC6AD192B4F07DD00015B18 /* OTelAttributeValue+Datadog.swift in Sources */, + D2C1A53929C4F2DF00946C31 /* DDNoOps.swift in Sources */, + D2C1A53A29C4F2DF00946C31 /* RequestBuilder.swift in Sources */, + D2C1A53B29C4F2DF00946C31 /* SpanTagsReducer.swift in Sources */, + 3CB012DE2B482E0400557951 /* NOPOTelSpan.swift in Sources */, + 61CE585B2B48174D00479510 /* SpanWriteContext.swift in Sources */, + D2C1A53D29C4F2DF00946C31 /* OTSpan.swift in Sources */, + D2C1A53E29C4F2DF00946C31 /* OTSpanContext.swift in Sources */, + 3C6C7FEC2B459AAA006F5CBC /* OTelTraceId+Datadog.swift in Sources */, + 3C6C7FEA2B459AAA006F5CBC /* OTelSpanBuilder.swift in Sources */, + D2C1A53F29C4F2DF00946C31 /* OTReference.swift in Sources */, + 3C6C7FF02B459AAA006F5CBC /* OTelSpanId+Datadog.swift in Sources */, + D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */, + 3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */, + 61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */, + 61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */, + D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */, + 3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */, + D2C1A54329C4F2DF00946C31 /* SpanEventEncoder.swift in Sources */, + 3C6C7FE82B459AAA006F5CBC /* OTelSpan.swift in Sources */, + D2C1A54429C4F2DF00946C31 /* SpanEventMapper.swift in Sources */, + D2C1A54529C4F2DF00946C31 /* DDFormat.swift in Sources */, + D2C1A54629C4F2DF00946C31 /* OTConstants.swift in Sources */, + D2C1A54729C4F2DF00946C31 /* DDSpanContext.swift in Sources */, + D2C1A54829C4F2DF00946C31 /* OTTracer.swift in Sources */, + D2C1A54929C4F2DF00946C31 /* (null) in Sources */, + D2C1A54A29C4F2DF00946C31 /* DDSpan.swift in Sources */, + D2C1A54B29C4F2DF00946C31 /* TracingWithLoggingIntegration.swift in Sources */, + D2C1A54C29C4F2DF00946C31 /* SpanEventBuilder.swift in Sources */, + D2C1A54D29C4F2DF00946C31 /* Warnings.swift in Sources */, + D2C1A54E29C4F2DF00946C31 /* OTFormat.swift in Sources */, + D22C5BCC2A98A5400024CC1F /* Baggages.swift in Sources */, + 3CFF5D4A2B555F4F00FC483A /* OTelTracerProvider.swift in Sources */, + D2C1A54F29C4F2DF00946C31 /* SpanSanitizer.swift in Sources */, + D2C1A55029C4F2DF00946C31 /* TraceFeature.swift in Sources */, + D2C1A55129C4F2DF00946C31 /* TracingURLSessionHandler.swift in Sources */, + D2C1A55229C4F2DF00946C31 /* DatadogTracer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2C1A55E29C4F2E800946C31 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2C1A55F29C4F2E800946C31 /* Casting+Tracing.swift in Sources */, + 3CC6AD1E2B4F07FB00015B18 /* OTelAttributeValue+DatadogTests.swift in Sources */, + D2C1A56029C4F2E800946C31 /* TracingURLSessionHandlerTests.swift in Sources */, + 619CE75F2A458CE1005588CB /* TraceConfigurationTests.swift in Sources */, + D2C1A56129C4F2E800946C31 /* WarningsTests.swift in Sources */, + D2C1A56229C4F2E800946C31 /* SpanEventBuilderTests.swift in Sources */, + 618C0FC12B482F6800266B38 /* SpanWriteContextTests.swift in Sources */, + D2C1A56329C4F2E800946C31 /* DDNoopTracerTests.swift in Sources */, + 61F3E3642BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, + D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, + 3C6C80002B459AF6006F5CBC /* OTelSpanTests.swift in Sources */, + 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, + D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */, + 3C5D636D2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */, + 3C3235A12B55387A000B4258 /* OTelSpanLinkTests.swift in Sources */, + 3C6C7FFF2B459AF6006F5CBC /* OTelTraceId+DatadogTests.swift in Sources */, + D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */, + D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */, + 3C6C7FFE2B459AF6006F5CBC /* OTelSpanId+DatadogTests.swift in Sources */, + D2C1A56929C4F2E800946C31 /* ActiveSpansPoolTests.swift in Sources */, + D2C1A56A29C4F2E800946C31 /* SpanSanitizerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6E0F27C50EAE00A62B57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D20605AA2874C1CD0047275C /* NetworkConnectionInfoPublisher.swift in Sources */, + 6128F56F2BA223A100D35B08 /* FeatureDataStore.swift in Sources */, + D29CDD3328211A2200F7DAA5 /* TLVBlock.swift in Sources */, + D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */, + A70A82662A935F210072F5DC /* BackgroundTaskCoordinator.swift in Sources */, + D2A1EE24287740B500D28DFB /* ApplicationStatePublisher.swift in Sources */, + D2CB6E2927C50EAE00A62B57 /* KronosInternetAddress.swift in Sources */, + 6128F5722BA223D100D35B08 /* DataStore+TLV.swift in Sources */, + D2CB6E2C27C50EAE00A62B57 /* KronosNTPPacket.swift in Sources */, + D2CB6E3127C50EAE00A62B57 /* FileWriter.swift in Sources */, + 3C0D5DE52A543E3500446CF9 /* EventGenerator.swift in Sources */, + D2EFA869286DA85700F1FAA6 /* DatadogContextProvider.swift in Sources */, + D2B3F04E282A85FD00C2B5EE /* DatadogCore.swift in Sources */, + 6128F5752BA3280300D35B08 /* DataStoreFileReader.swift in Sources */, + D2303A0B298D5412001A1FA3 /* AsyncWriter.swift in Sources */, + D224430729E95C2E00274EC7 /* MessageBus.swift in Sources */, + 61F930BF2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */, + 6128F5782BA32DE500D35B08 /* DataStoreFileWriter.swift in Sources */, + D2CB6E3627C50EAE00A62B57 /* ObjcAppLaunchHandler.m in Sources */, + D2CB6E3C27C50EAE00A62B57 /* Retrying.swift in Sources */, + D29A9F9129DD8771005C54A4 /* CITestIntegration.swift in Sources */, + D20605B32874E1660047275C /* CarrierInfoPublisher.swift in Sources */, + D2A1EE33287DA51900D28DFB /* UserInfoPublisher.swift in Sources */, + D2CB6E4327C50EAE00A62B57 /* ObjcExceptionHandler.m in Sources */, + D2A7841029A53B2F003B03BB /* Directory.swift in Sources */, + 61DA8CB028620C760074A606 /* Cryptography.swift in Sources */, + D2DC4BF727F484AA00E4FB96 /* DataEncryption.swift in Sources */, + 614396732A67D74F00197326 /* BatchMetrics.swift in Sources */, + D29294E1291D5ED500F8EFF9 /* ApplicationVersionPublisher.swift in Sources */, + D20605A7287476230047275C /* ServerOffsetPublisher.swift in Sources */, + D21C26C628A3B49C005DD405 /* FeatureStorage.swift in Sources */, + D2CB6E5527C50EAE00A62B57 /* KronosNSTimer+ClosureKit.swift in Sources */, + D2CB6E6627C50EAE00A62B57 /* Reader.swift in Sources */, + D2CB6E6927C50EAE00A62B57 /* KronosDNSResolver.swift in Sources */, + D286626F2A43487500852CE3 /* Datadog.swift in Sources */, + 61F930C32BA1C41A005F0EE2 /* TLVBlockReader.swift in Sources */, + D2A7841229A53B2F003B03BB /* File.swift in Sources */, + D2CB6E7627C50EAE00A62B57 /* KronosClock.swift in Sources */, + D2CB6E7727C50EAE00A62B57 /* DataReader.swift in Sources */, + 617699192A860D9D0030022B /* HTTPClient.swift in Sources */, + D2FB1255292E0E99005B13F8 /* TrackingConsentPublisher.swift in Sources */, + D26C49C0288982DA00802B2D /* FeatureUpload.swift in Sources */, + D2CB6E8127C50EAE00A62B57 /* DataUploader.swift in Sources */, + D2CB6E8827C50EAE00A62B57 /* FileReader.swift in Sources */, + D2CB6E8D27C50EAE00A62B57 /* KronosNTPProtocol.swift in Sources */, + D2CB6E9127C50EAE00A62B57 /* KronosTimeFreeze.swift in Sources */, + D2FB125E292FBB56005B13F8 /* Datadog+Internal.swift in Sources */, + 61DA8CAA28609C5B0074A606 /* Directories.swift in Sources */, + D2CB6E9727C50EAE00A62B57 /* DataUploadStatus.swift in Sources */, + D255382A288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */, + D2CB6E9927C50EAE00A62B57 /* DataUploadWorker.swift in Sources */, + D2CB6E9A27C50EAE00A62B57 /* KronosTimeStorage.swift in Sources */, + D2CB6E9B27C50EAE00A62B57 /* FilesOrchestrator.swift in Sources */, + D2553827288F0B1A00727FAD /* BatteryStatusPublisher.swift in Sources */, + D20605A4287464F40047275C /* ContextValuePublisher.swift in Sources */, + D2CB6EA727C50EAE00A62B57 /* Versioning.swift in Sources */, + D2CB6EA827C50EAE00A62B57 /* URLSessionClient.swift in Sources */, + D2CB6EB327C50EAE00A62B57 /* KronosNTPClient.swift in Sources */, + D2CB6EBA27C50EAE00A62B57 /* DataUploadConditions.swift in Sources */, + D2CB6EBF27C50EAE00A62B57 /* KronosData+Bytes.swift in Sources */, + D2CB6EC427C50EAE00A62B57 /* DataUploadDelay.swift in Sources */, + D2CB6EC727C50EAE00A62B57 /* PerformancePreset.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6ED627C520D400A62B57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D28F836929C9E71D00EF8EA2 /* DDSpanTests.swift in Sources */, + 61B8BA92281812F60068AFF4 /* KronosInternetAddressTests.swift in Sources */, + 3C1890162ABDE9C000CE9E73 /* DDURLSessionInstrumentationTests+apiTests.m in Sources */, + 6134CDB22A691E850061CCD9 /* BatchMetricsTests.swift in Sources */, + 61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */, + D22743E029DEB8B5001A7EF9 /* VitalCPUReaderTests.swift in Sources */, + D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */, + D24C9C4E29A7BA41002057CF /* LogsMocks.swift in Sources */, + D2CB6EDE27C520D400A62B57 /* RUMEventMatcher.swift in Sources */, + D2CB6EE027C520D400A62B57 /* SpanMatcher.swift in Sources */, + D2552AF32BBC47D300A45725 /* WebRecordIntegrationTests.swift in Sources */, + 6179DB572B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */, + 61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */, + D2DC4BBD27F234E000E4FB96 /* CITestIntegrationTests.swift in Sources */, + D2CB6EE427C520D400A62B57 /* FeatureTests.swift in Sources */, + A7EA11622AB0CE6C00C73970 /* DDUIKitRUMActionsPredicateTests.swift in Sources */, + D2CB6EE527C520D400A62B57 /* DataUploadConditionsTests.swift in Sources */, + D2CB6EE627C520D400A62B57 /* DateFormattingTests.swift in Sources */, + 61DCC84B2C05D4D600CB59E5 /* RUMSessionEndedMetricIntegrationTests.swift in Sources */, + D2CB6EE727C520D400A62B57 /* FileTests.swift in Sources */, + 610ABD4D2A6930CA00AFEA34 /* CoreTelemetryIntegrationTests.swift in Sources */, + D2CB6EEA27C520D400A62B57 /* LogMatcher.swift in Sources */, + D2CB6EEC27C520D400A62B57 /* CustomObjcViewController.m in Sources */, + D2EFA876286E011900F1FAA6 /* DatadogContextProviderTests.swift in Sources */, + 614B78F2296D7B63009C6B92 /* LowPowerModePublisherTests.swift in Sources */, + D2CB6EEE27C520D400A62B57 /* DDErrorTests.swift in Sources */, + 3C0D5DE32A543DC900446CF9 /* EventGeneratorTests.swift in Sources */, + D25CFAA229C8644E00E3A43D /* Casting+Tracing.swift in Sources */, + D2CB6EF227C520D400A62B57 /* KronosTimeStorageTests.swift in Sources */, + D2CB6EF427C520D400A62B57 /* FileWriterTests.swift in Sources */, + D2CB6EFE27C520D400A62B57 /* RUMMonitorConfigurationTests.swift in Sources */, + D2CB6F0027C520D400A62B57 /* RUMSessionMatcher.swift in Sources */, + A728ADB12934EB0C00397996 /* DDW3CHTTPHeadersWriter+apiTests.m in Sources */, + 6167E6DE2B811A8300C3CA2D /* AppHangsMonitoringTests.swift in Sources */, + D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */, + D24C9C7229A7D57A002057CF /* DirectoriesMock.swift in Sources */, + 61DA8CB3286215DE0074A606 /* CryptographyTests.swift in Sources */, + 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 */, + D2CB6F0C27C520D400A62B57 /* KronosNTPPacketTests.swift in Sources */, + D2CB6F0E27C520D400A62B57 /* DDRUMMonitorTests.swift in Sources */, + 612C13D12AA772FA0086B5D1 /* SRRequestMatcher.swift in Sources */, + D24C9C6529A7CB7D002057CF /* CrashLogReceiverTests.swift in Sources */, + D29A9FD129DDC590005C54A4 /* RUMFeatureTests.swift in Sources */, + D2CB6F1027C520D400A62B57 /* DDNSURLSessionDelegateTests.swift in Sources */, + 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 */, + D2CB6F1927C520D400A62B57 /* RequestBuilderTests.swift in Sources */, + D2CB6F1A27C520D400A62B57 /* FileReaderTests.swift in Sources */, + D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */, + D2A1EE39287EEB7600D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */, + D2CB6F1D27C520D400A62B57 /* DataUploaderTests.swift in Sources */, + D2A1EE35287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift in Sources */, + D22743E129DEB8B5001A7EF9 /* VitalRefreshRateReaderTests.swift in Sources */, + D2CB6F2027C520D400A62B57 /* DatadogConfigurationTests.swift in Sources */, + D2CB6F2127C520D400A62B57 /* URLSessionClientTests.swift in Sources */, + D2CB6F2227C520D400A62B57 /* DatadogTests.swift in Sources */, + 61DA8CAD2861C3720074A606 /* DirectoriesTests.swift in Sources */, + 614798972A459AA80095CB02 /* DDTraceTests.swift in Sources */, + D2CB6F2627C520D400A62B57 /* DataUploadDelayTests.swift in Sources */, + D2CB6F2827C520D400A62B57 /* DataUploadWorkerTests.swift in Sources */, + D2CB6F2B27C520D400A62B57 /* CrashContextProviderTests.swift in Sources */, + 49274907288048B800ECD49B /* InternalProxyTests.swift in Sources */, + D2552AF62BBC492600A45725 /* WebEventIntegrationTests.swift in Sources */, + D29A9FC529DDB719005C54A4 /* RUMInternalProxyTests.swift in Sources */, + D22743EA29DEC9A9001A7EF9 /* RUMDataModelMocks.swift in Sources */, + D22743E529DEB934001A7EF9 /* UIViewControllerSwizzlerTests.swift in Sources */, + D2CB6F2C27C520D400A62B57 /* JSONEncoderTests.swift in Sources */, + D2CB6F3027C520D400A62B57 /* DatadogExtensions.swift in Sources */, + D2CB6F3227C520D400A62B57 /* JSONDataMatcher.swift in Sources */, + 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 */, + D2A1EE452886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */, + 61A2CC222A443D330000FF25 /* DDRUMConfigurationTests.swift in Sources */, + 6128F5852BA8CAAB00D35B08 /* DataStoreFileWriterTests.swift in Sources */, + 6176991C2A86121B0030022B /* HTTPClientMock.swift in Sources */, + 6128F57C2BA35D6200D35B08 /* FeatureDataStoreTests.swift in Sources */, + 6128F57F2BA8A3A000D35B08 /* DataStore+TLVTests.swift in Sources */, + 3CCCA5C82ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift in Sources */, + D29294E4291D652D00F8EFF9 /* ApplicationVersionPublisherTests.swift in Sources */, + A7DA18052AB0C91300F76337 /* DDUIKitRUMViewsPredicateTests.swift in Sources */, + A79B0F65292BD074008742B3 /* DDB3HTTPHeadersWriter+apiTests.m in Sources */, + D2CB6F4327C520D400A62B57 /* DDLogsTests.swift in Sources */, + D2CB6F4527C520D400A62B57 /* TracerTests.swift in Sources */, + D2CB6F4627C520D400A62B57 /* CoreMocks.swift in Sources */, + D2CB6F4827C520D400A62B57 /* CrashReportingFeatureMocks.swift in Sources */, + D22743EC29DEC9E6001A7EF9 /* Casting+RUM.swift in Sources */, + D2CB6F4D27C520D400A62B57 /* DataUploadStatusTests.swift in Sources */, + D2CB6F4F27C520D400A62B57 /* RetryingTests.swift in Sources */, + A7CA21812BEBB1E800732571 /* AppBackgroundTaskCoordinatorTests.swift in Sources */, + D2CB6F5027C520D400A62B57 /* DDDatadogTests.swift in Sources */, + D2B3F0452823EE8400C2B5EE /* TLVBlockTests.swift in Sources */, + D2CB6F5327C520D400A62B57 /* DirectoryTests.swift in Sources */, + 618353BD2A69470A0085F84A /* CoreMetricsIntegrationTests.swift in Sources */, + 6176991F2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift in Sources */, + D2B3F053282E827B00C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */, + D20605BA2875729E0047275C /* ContextValuePublisherMock.swift in Sources */, + D22743E729DEB953001A7EF9 /* UIApplicationSwizzlerTests.swift in Sources */, + 3CF673372B4807490016CE17 /* OTelSpanTests.swift in Sources */, + D25CFAA029C860E300E3A43D /* TracingFeatureMocks.swift in Sources */, + D2CB6F5F27C520D400A62B57 /* DDNSURLSessionDelegate+apiTests.m in Sources */, + 6128F58B2BA9860B00D35B08 /* DataStoreFileReaderTests.swift in Sources */, + D224430E29E95D6700274EC7 /* CrashReportReceiverTests.swift in Sources */, + 61A1A44A29643254007909E7 /* DatadogCoreProxy.swift in Sources */, + D20605C42875895C0047275C /* KronosClockMock.swift in Sources */, + 6147989D2A459E2B0095CB02 /* DDTrace+apiTests.m in Sources */, + D2CB6F6427C520D400A62B57 /* LoggerTests.swift in Sources */, + D29A9FD929DDC687005C54A4 /* UIKitRUMViewsPredicateTests.swift in Sources */, + D2CB6F6627C520D400A62B57 /* RUMMonitorTests.swift in Sources */, + 61E8C5092B28898800E709B4 /* StartingRUMSessionTests.swift in Sources */, + D22743DF29DEB8B5001A7EF9 /* VitalMemoryReaderTests.swift in Sources */, + D2CB6F6827C520D400A62B57 /* SwiftUIExtensionsTests.swift in Sources */, + D2CB6F6A27C520D400A62B57 /* DDRUMMonitor+apiTests.m in Sources */, + D22743DD29DEB8B5001A7EF9 /* VitalInfoTests.swift in Sources */, + D2CB6F7027C520D400A62B57 /* UIKitMocks.swift in Sources */, + D2CB6F7327C520D400A62B57 /* CoreTelephonyMocks.swift in Sources */, + D20605B7287572640047275C /* DatadogContextProviderMock.swift in Sources */, + D2CB6F7527C520D400A62B57 /* UIKitExtensionsTests.swift in Sources */, + 61F930C92BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */, + D21831562B6A57530012B3A0 /* NetworkInstrumentationIntegrationTests.swift in Sources */, + 61DA8CB928647A500074A606 /* InternalLoggerTests.swift in Sources */, + D2CB6F7C27C520D400A62B57 /* CrashReporterTests.swift in Sources */, + 613F9C192BAC3527007C7606 /* DatadogCore+FeatureDataStoreTests.swift in Sources */, + 6167E70F2B83502200C3CA2D /* DatadogCore+FeatureDirectoriesTests.swift in Sources */, + D2CB6F7D27C520D400A62B57 /* CrashContextTests.swift in Sources */, + D28F836629C9E6A200EF8EA2 /* DatadogTraceFeatureTests.swift in Sources */, + 612C13D72AAB35EB0086B5D1 /* SRSegmentMatcher.swift in Sources */, + 6147989A2A459B2E0095CB02 /* DDTraceConfigurationTests.swift in Sources */, + D2CB6F7E27C520D400A62B57 /* OTSpanTests.swift in Sources */, + D2CB6F7F27C520D400A62B57 /* DDDatadog+apiTests.m in Sources */, + D2CB6F8027C520D400A62B57 /* TracingWithLoggingIntegrationTests.swift in Sources */, + D29A9FDB29DDC6D1005C54A4 /* RUMEventFileOutputTests.swift in Sources */, + D22743E229DEB90B001A7EF9 /* RUMDebuggingTests.swift in Sources */, + D2A1EE3F2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */, + D2CB6F8327C520D400A62B57 /* DDConfiguration+apiTests.m in Sources */, + D2CB6F8427C520D400A62B57 /* DatadogTestsObserverLoader.m in Sources */, + D2CB6F8527C520D400A62B57 /* PerformancePresetTests.swift in Sources */, + D2553808288AA84F00727FAD /* UploadMock.swift in Sources */, + 61A2CC252A44454D0000FF25 /* DDRUMTests.swift in Sources */, + D21C26D228A64599005DD405 /* MessageBusTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6F9727C5217A00A62B57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 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 */, + A728ADAC2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift in Sources */, + A79B0F67292BD7CC008742B3 /* B3HTTPHeadersWriter+objc.swift in Sources */, + D2CB6F9E27C5217A00A62B57 /* Datadog+objc.swift in Sources */, + D2CB6F9F27C5217A00A62B57 /* Logs+objc.swift in Sources */, + 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, + D2CB6FA027C5217A00A62B57 /* Trace+objc.swift in Sources */, + D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */, + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, + D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */, + 3CA852652BF2148400B52CBA /* TraceContextInjection+objc.swift in Sources */, + D2CB6FA327C5217A00A62B57 /* OTSpan+objc.swift in Sources */, + D2CB6FA427C5217A00A62B57 /* DDURLSessionDelegate+objc.swift in Sources */, + D2CB6FA527C5217A00A62B57 /* RUM+objc.swift in Sources */, + D2CB6FA627C5217A00A62B57 /* OTSpanContext+objc.swift in Sources */, + D2CB6FA827C5217A00A62B57 /* DatadogConfiguration+objc.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FBF27C5348200A62B57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D214DA8C29DF2D6B004D0AE8 /* CrashReportSender.swift in Sources */, + D293302D2A137DAD0029C9EA /* CrashReportingFeature.swift in Sources */, + D2CB6FC027C5348200A62B57 /* DDCrashReportBuilder.swift in Sources */, + D2CB6FC127C5348200A62B57 /* DDCrashReportExporter.swift in Sources */, + D2CB6FC227C5348200A62B57 /* CrashReportMinifier.swift in Sources */, + D214DA8429DF2D5E004D0AE8 /* CrashReporting.swift in Sources */, + D2CB6FC327C5348200A62B57 /* PLCrashReporterIntegration.swift in Sources */, + D214DA8229DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */, + 6167E7042B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */, + D214DA8D29DF2D6B004D0AE8 /* CrashContext.swift in Sources */, + D2CB6FC427C5348200A62B57 /* CrashReport.swift in Sources */, + D214DA8E29DF2D6B004D0AE8 /* CrashContextProvider.swift in Sources */, + D2CB6FC527C5348200A62B57 /* SwiftExtensions.swift in Sources */, + D2CB6FC627C5348200A62B57 /* PLCrashReporterPlugin.swift in Sources */, + D2CB6FC727C5348200A62B57 /* ThirdPartyCrashReporter.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2CB6FD827C5352300A62B57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CB6FD927C5352300A62B57 /* DDCrashReportBuilderTests.swift in Sources */, + D2CB6FDB27C5352300A62B57 /* SwiftExtensionTests.swift in Sources */, + D2CB6FDC27C5352300A62B57 /* PLCrashReporterIntegrationTests.swift in Sources */, + D2CB6FDD27C5352300A62B57 /* CrashReportMinifierTests.swift in Sources */, + D2CB6FDE27C5352300A62B57 /* DDCrashReportExporterTests.swift in Sources */, + D2CB6FE027C5352300A62B57 /* CrashReportingPluginTests.swift in Sources */, + D2CB6FE127C5352300A62B57 /* CrashReportTests.swift in Sources */, + D2CB6FE227C5352300A62B57 /* Mocks.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2357298D57AA00C6C7E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 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 */, + D2EBEE3429BA161100B15732 /* B3HTTPHeaders.swift in Sources */, + D23354FD2A42E32000AFCAE2 /* InternalExtended.swift in Sources */, + 619F5CED2BF508A4004BFE70 /* GlobalRUMAttributes.swift in Sources */, + D2DA235B298D57AA00C6C7E6 /* DynamicCodingKey.swift in Sources */, + D2BEEDB32B335DA90065F3AC /* URLSessionTaskDelegateSwizzler.swift in Sources */, + D2DA235C298D57AA00C6C7E6 /* FeatureRequestBuilder.swift in Sources */, + D2160CE629C0DFEE00FAA9A5 /* MethodSwizzler.swift in Sources */, + D2160CCA29C0DED100FAA9A5 /* DatadogURLSessionDelegate.swift in Sources */, + D2EBEE3729BA161100B15732 /* HTTPHeadersWriter.swift in Sources */, + D2432CFA29EDB22C00D93657 /* Flushable.swift in Sources */, + D2DA235D298D57AA00C6C7E6 /* AttributesSanitizer.swift in Sources */, + D2DA235E298D57AA00C6C7E6 /* DatadogFeature.swift in Sources */, + D2BEEDBB2B3363900065F3AC /* NetworkInstrumentationSwizzler.swift in Sources */, + 3CBDE6752AA08C2F00F6A7B6 /* URLSessionInstrumentation.swift in Sources */, + D2DA235F298D57AA00C6C7E6 /* CarrierInfo.swift in Sources */, + D2DA2360298D57AA00C6C7E6 /* DDError.swift in Sources */, + D2DA2361298D57AA00C6C7E6 /* AnyCodable.swift in Sources */, + D29A9F9629DDB1DB005C54A4 /* UIKitExtensions.swift in Sources */, + 6167E6E92B8122E900C3CA2D /* BacktraceReport.swift in Sources */, + D2BEEDB62B3360830065F3AC /* URLSessionSwizzler.swift in Sources */, + D2EBEE3329BA161100B15732 /* TraceID.swift in Sources */, + D2EBEE2F29BA161100B15732 /* W3CHTTPHeaders.swift in Sources */, + 6167E6F72B81E94C00C3CA2D /* DDThread.swift in Sources */, + D2BEEDAD2B3356710065F3AC /* URLSessionTaskSwizzler.swift in Sources */, + D2DA2363298D57AA00C6C7E6 /* BatteryStatus.swift in Sources */, + D2EBEE3829BA161100B15732 /* TracingHTTPHeaders.swift in Sources */, + 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 */, + D2DA2368298D57AA00C6C7E6 /* DataFormat.swift in Sources */, + D2160CEE29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */, + D2160C9F29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, + D2DA2369298D57AA00C6C7E6 /* AnyEncodable.swift in Sources */, + D2DA236A298D57AA00C6C7E6 /* DatadogExtended.swift in Sources */, + D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */, + 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, + 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 */, + D2DA236E298D57AA00C6C7E6 /* InternalLogger.swift in Sources */, + 6174D6142BFDF16C00EC7469 /* BundleType.swift in Sources */, + D2DA236F298D57AA00C6C7E6 /* DateFormatting.swift in Sources */, + 3C9B27262B9F174700569C07 /* SpanID.swift in Sources */, + D2216EC12A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, + D2DA2370298D57AA00C6C7E6 /* AnyDecodable.swift in Sources */, + 6167E6E32B81207200C3CA2D /* DDCrashReport.swift in Sources */, + D2160CC629C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, + 6167E6FE2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */, + D2DA2372298D57AA00C6C7E6 /* DD.swift in Sources */, + D2160C9B29C0DE5700FAA9A5 /* FirstPartyHosts.swift in Sources */, + D2EBEE3029BA161100B15732 /* TracePropagationHeadersReader.swift in Sources */, + D2DA2373298D57AA00C6C7E6 /* ReadWriteLock.swift in Sources */, + D2EBEE3229BA161100B15732 /* W3CHTTPHeadersReader.swift in Sources */, + A7FA98CF2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */, + 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 */, + D2DA2377298D57AA00C6C7E6 /* URLRequestBuilder.swift in Sources */, + 3CA852602BF2073800B52CBA /* TraceContextInjection.swift in Sources */, + D2DA2378298D57AA00C6C7E6 /* Attributes.swift in Sources */, + D20731CC29A52E6000ECBF94 /* Sampler.swift in Sources */, + D2EBEE2E29BA161100B15732 /* TracePropagationHeadersWriter.swift in Sources */, + D2DA2379298D57AA00C6C7E6 /* AnyDecoder.swift in Sources */, + D2DA237A298D57AA00C6C7E6 /* FeatureMessage.swift in Sources */, + 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 */, + D2C9A26A2C0F3F5A007526F5 /* SessionReplayConfiguration.swift in Sources */, + 6167E6FA2B81E95900C3CA2D /* BinaryImage.swift in Sources */, + 6174D60D2BFDDEDF00EC7469 /* SDKMetricFields.swift in Sources */, + D2DA237E298D57AA00C6C7E6 /* AnyEncoder.swift in Sources */, + D2A783D529A530A0003B03BB /* SwiftExtensions.swift in Sources */, + 3C0D5DD82A543B3B00446CF9 /* Event.swift in Sources */, + 3CBDE68B2AA0C47300F6A7B6 /* URLSessionTask+Tracking.swift in Sources */, + D270CDDE2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */, + D22F06DA29DAFD500026CC3C /* TimeInterval+Convenience.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2DA2386298D588800C6C7E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2BEEDAF2B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift in Sources */, + D26416B62A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */, + 61F3E3662BC595F600C7881E /* HTTPHeadersReaderTests.swift in Sources */, + D2EBEE3C29BA163E00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */, + D21AE6BC29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */, + D2DA23A3298D58F400C6C7E6 /* AnyEncodableTests.swift in Sources */, + 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 */, + D2EBEE3D29BA163E00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */, + 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 */, + D2EBEE3E29BA163E00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */, + E1C853142AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */, + D2160CE929C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */, + D2216EC32A96649500ADAEC8 /* FeatureBaggageTests.swift in Sources */, + D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, + 6156A9072BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */, + D2F44FB8299AA1DA0074B0D9 /* DataCompressionTests.swift in Sources */, + D2160CE029C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, + D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, + D2BEEDB82B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */, + 3CCECDB22BC68A0A0013C125 /* SpanIDTests.swift in Sources */, + D2181A8E2B051B7900A518C0 /* URLSessionSwizzlerTests.swift in Sources */, + D2A783DA29A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */, + D2D36DCB2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift in Sources */, + 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; + }; + D2DA23B0298D59DC00C6C7E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2BEEDB02B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift in Sources */, + D26416B72A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */, + 61F3E3672BC595F600C7881E /* HTTPHeadersReaderTests.swift in Sources */, + D2EBEE4029BA163F00B15732 /* B3HTTPHeadersWriterTests.swift in Sources */, + D21AE6BD29E5EDAF0064BF29 /* TelemetryTests.swift in Sources */, + D2DA23B1298D59DC00C6C7E6 /* AnyEncodableTests.swift in Sources */, + 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 */, + D2EBEE4129BA163F00B15732 /* W3CHTTPHeadersWriterTests.swift in Sources */, + 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 */, + D2DA23B6298D59DC00C6C7E6 /* AnyCoderTests.swift in Sources */, + E1C853152AA9B9A300C74BCF /* TelemetryMocks.swift in Sources */, + D2EBEE4229BA163F00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */, + D2216EC42A96649700ADAEC8 /* FeatureBaggageTests.swift in Sources */, + D2160CDD29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, + 6156A9082BF75A7C00DF66C3 /* ImmutableRequestTests.swift in Sources */, + D2F44FB9299AA1DB0074B0D9 /* DataCompressionTests.swift in Sources */, + D2160CE129C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, + D2EBEE3F29BA163F00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, + D2BEEDB92B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */, + 3CCECDB32BC68A0A0013C125 /* SpanIDTests.swift in Sources */, + D2181A8F2B051B7900A518C0 /* URLSessionSwizzlerTests.swift in Sources */, + D2A783D929A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */, + D2D36DCC2AC6DCCA0021F28A /* DatadogCoreProtocolTests.swift in Sources */, + 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; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3C41693E29FBF5BB0042B9D2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D257953D298ABA65008A1BE5 /* TestUtilities iOS */; + targetProxy = 3C41693D29FBF5BB0042B9D2 /* PBXContainerItemProxy */; + }; + 3C41694029FBF5F20042B9D2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = 3C41693F29FBF5F20042B9D2 /* PBXContainerItemProxy */; + }; + 3C41694229FBF6100042B9D2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = 3C41694129FBF6100042B9D2 /* PBXContainerItemProxy */; + }; + 3C4D5FEF2A0115C600F1FF78 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2C1A53329C4F2DF00946C31 /* DatadogTrace tvOS */; + targetProxy = 3C4D5FEE2A0115C600F1FF78 /* PBXContainerItemProxy */; + }; + 3C4D5FF12A0115CB00F1FF78 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = 3C4D5FF02A0115CB00F1FF78 /* PBXContainerItemProxy */; + }; + 3C9C6BB729F7C0C000581C43 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = 3C9C6BB629F7C0C000581C43 /* PBXContainerItemProxy */; + }; + 3CE11A0829F7BE0500202522 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3CE119FD29F7BE0000202522 /* DatadogWebViewTracking iOS */; + targetProxy = 3CE11A0729F7BE0500202522 /* PBXContainerItemProxy */; + }; + 61133C732423993200786299 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61133B81242393DE00786299 /* DatadogCore iOS */; + targetProxy = 61133C722423993200786299 /* PBXContainerItemProxy */; + }; + 6133D1E62A6ED9E100384BEF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = 6133D1E72A6ED9E100384BEF /* PBXContainerItemProxy */; + }; + 6133D1F72A6EDB7700384BEF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = 6133D1F82A6EDB7700384BEF /* PBXContainerItemProxy */; + }; + 6133D1F92A6EDB7700384BEF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D257953D298ABA65008A1BE5 /* TestUtilities iOS */; + targetProxy = 6133D1FA2A6EDB7700384BEF /* PBXContainerItemProxy */; + }; + 6133D20A2A6EDBAE00384BEF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6133D1E52A6ED9E100384BEF /* DatadogSessionReplay iOS */; + targetProxy = 6133D2092A6EDBAE00384BEF /* PBXContainerItemProxy */; + }; + 61441C5A24619A08003D8BB8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61441C0124616DE9003D8BB8 /* Example iOS */; + targetProxy = 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */; + }; + 6158155B2AB4534F002C60D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61441C0124616DE9003D8BB8 /* Example iOS */; + targetProxy = 6158155A2AB4534F002C60D7 /* PBXContainerItemProxy */; + }; + 618F9846265BC486009959F8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6199362A265BA958009D7EA8 /* E2E */; + targetProxy = 618F9845265BC486009959F8 /* PBXContainerItemProxy */; + }; + 61993659265BB6A6009D7EA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61133B81242393DE00786299 /* DatadogCore iOS */; + targetProxy = 61993658265BB6A6009D7EA8 /* PBXContainerItemProxy */; + }; + 6199365D265BB6A6009D7EA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61B7885325C180CB002675B5 /* DatadogCrashReporting iOS */; + targetProxy = 6199365C265BB6A6009D7EA8 /* PBXContainerItemProxy */; + }; + 6199366B265BBEDC009D7EA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6199362A265BA958009D7EA8 /* E2E */; + targetProxy = 6199366A265BBEDC009D7EA8 /* PBXContainerItemProxy */; + }; + 61A2CC292A4449210000FF25 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D29A9F3329DD84AA005C54A4 /* DatadogRUM iOS */; + targetProxy = 61A2CC282A4449210000FF25 /* PBXContainerItemProxy */; + }; + 61A2CC2E2A4449300000FF25 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23F8E4D29DDCD28001CFAE8 /* DatadogRUM tvOS */; + targetProxy = 61A2CC2D2A4449300000FF25 /* PBXContainerItemProxy */; + }; + 61B7885F25C180CB002675B5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61B7885325C180CB002675B5 /* DatadogCrashReporting iOS */; + targetProxy = 61B7885E25C180CB002675B5 /* PBXContainerItemProxy */; + }; + D206BB882A41CA6800F43BA2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D207317B29A5226A00ECBF94 /* DatadogLogs iOS */; + targetProxy = D206BB872A41CA6800F43BA2 /* PBXContainerItemProxy */; + }; + D206BB8D2A41CA7000F43BA2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D20731A429A5279D00ECBF94 /* DatadogLogs tvOS */; + targetProxy = D206BB8C2A41CA7000F43BA2 /* PBXContainerItemProxy */; + }; + D207318629A5226B00ECBF94 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = D207317B29A5226A00ECBF94 /* DatadogLogs iOS */; + targetProxy = D207318529A5226B00ECBF94 /* PBXContainerItemProxy */; + }; + D207319A29A5232A00ECBF94 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D207319929A5232A00ECBF94 /* PBXContainerItemProxy */; + }; + D22A031B29F7DAA9002C02C6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D20731A429A5279D00ECBF94 /* DatadogLogs tvOS */; + targetProxy = D22A031A29F7DAA9002C02C6 /* PBXContainerItemProxy */; + }; + D22A031D29F7DABE002C02C6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23F8E4D29DDCD28001CFAE8 /* DatadogRUM tvOS */; + targetProxy = D22A031C29F7DABE002C02C6 /* PBXContainerItemProxy */; + }; + D2303A07298D5317001A1FA3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D2303A06298D5317001A1FA3 /* PBXContainerItemProxy */; + }; + D231F7B02A00FF28000D6239 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D231F7AF2A00FF28000D6239 /* PBXContainerItemProxy */; + }; + D231F7B22A00FF2F000D6239 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D231F7B12A00FF2F000D6239 /* PBXContainerItemProxy */; + }; + D231F7B42A00FF8F000D6239 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61133B81242393DE00786299 /* DatadogCore iOS */; + targetProxy = D231F7B32A00FF8F000D6239 /* PBXContainerItemProxy */; + }; + D231F7B62A00FF9A000D6239 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61B7885325C180CB002675B5 /* DatadogCrashReporting iOS */; + targetProxy = D231F7B52A00FF9A000D6239 /* PBXContainerItemProxy */; + }; + D231F7B82A00FFA3000D6239 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2CB6FBA27C5348200A62B57 /* DatadogCrashReporting tvOS */; + targetProxy = D231F7B72A00FFA3000D6239 /* PBXContainerItemProxy */; + }; + D23F8ED029DDCD5C001CFAE8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D23F8ECF29DDCD5C001CFAE8 /* PBXContainerItemProxy */; + }; + D240685727CF5D0100C04F44 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2CB6E0A27C50EAE00A62B57 /* DatadogCore tvOS */; + targetProxy = D240685627CF5D0100C04F44 /* PBXContainerItemProxy */; + }; + D240686D27CF687200C04F44 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D24067F827CE6C9E00C04F44 /* Example tvOS */; + targetProxy = D240686C27CF687200C04F44 /* PBXContainerItemProxy */; + }; + D25CFA9B29C4F41F00E3A43D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2C1A53329C4F2DF00946C31 /* DatadogTrace tvOS */; + targetProxy = D25CFA9A29C4F41F00E3A43D /* PBXContainerItemProxy */; + }; + D25EE93E29C4C3C300CE3839 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = D25EE93329C4C3C300CE3839 /* DatadogTrace iOS */; + targetProxy = D25EE93D29C4C3C300CE3839 /* PBXContainerItemProxy */; + }; + D26F741529ACBDAD00D25622 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D26F741429ACBDAD00D25622 /* PBXContainerItemProxy */; + }; + D28D5D5427C53A60008E72D0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2CB6FBA27C5348200A62B57 /* DatadogCrashReporting tvOS */; + targetProxy = D28D5D5327C53A60008E72D0 /* PBXContainerItemProxy */; + }; + D29A9F3E29DD84AB005C54A4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D29A9F3329DD84AA005C54A4 /* DatadogRUM iOS */; + targetProxy = D29A9F3D29DD84AB005C54A4 /* PBXContainerItemProxy */; + }; + D29A9F4E29DD8525005C54A4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D29A9F4D29DD8525005C54A4 /* PBXContainerItemProxy */; + }; + D2A434A52A8E3F900028E329 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6133D1E52A6ED9E100384BEF /* DatadogSessionReplay iOS */; + targetProxy = D2A434A42A8E3F900028E329 /* PBXContainerItemProxy */; + }; + D2A783E329A53414003B03BB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D2A783E229A53414003B03BB /* PBXContainerItemProxy */; + }; + D2C1A51129C4C4EF00946C31 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D2C1A51029C4C4EF00946C31 /* PBXContainerItemProxy */; + }; + D2C1A52C29C4C92800946C31 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D25EE93329C4C3C300CE3839 /* DatadogTrace iOS */; + targetProxy = D2C1A52B29C4C92800946C31 /* PBXContainerItemProxy */; + }; + D2C1A57729C4F30000946C31 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D2C1A57629C4F30000946C31 /* PBXContainerItemProxy */; + }; + D2CB6FB627C5234300A62B57 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2CB6E0A27C50EAE00A62B57 /* DatadogCore tvOS */; + targetProxy = D2CB6FB527C5234300A62B57 /* PBXContainerItemProxy */; + }; + D2DA2390298D588A00C6C7E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = D23039A4298D513C001A1FA3 /* DatadogInternal iOS */; + targetProxy = D2DA238F298D588A00C6C7E6 /* PBXContainerItemProxy */; + }; + D2DA23D3298D620F00C6C7E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D2DA2355298D57AA00C6C7E6 /* DatadogInternal tvOS */; + targetProxy = D2DA23D2298D620F00C6C7E6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 61441C0A24616DE9003D8BB8 /* Main iOS.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 61441C0B24616DE9003D8BB8 /* Base */, + ); + name = "Main iOS.storyboard"; + sourceTree = ""; + }; + 61993638265BA95A009D7EA8 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 61993639265BA95A009D7EA8 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + D240688427CFA64A00C04F44 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D240688527CFA64A00C04F44 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3CE11A1329F7BE0B00202522 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTracking; + PRODUCT_NAME = DatadogWebViewTracking; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3CE11A1429F7BE0B00202522 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTracking; + PRODUCT_NAME = DatadogWebViewTracking; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3CE11A1529F7BE0B00202522 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTracking; + PRODUCT_NAME = DatadogWebViewTracking; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 3CE11A1629F7BE0B00202522 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTrackingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3CE11A1729F7BE0B00202522 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTrackingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 3CE11A1829F7BE0B00202522 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogWebViewTrackingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 61133B94242393DE00786299 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 61133B95242393DE00786299 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 61133B97242393DE00786299 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 61133B98242393DE00786299 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 61133B9A242393DE00786299 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; + }; + name = Debug; + }; + 61133B9B242393DE00786299 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; + }; + name = Release; + }; + 61133C02242397DA00786299 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 61133C03242397DA00786299 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 6133D1F22A6ED9E100384BEF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplay; + PRODUCT_NAME = DatadogSessionReplay; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6133D1F32A6ED9E100384BEF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplay; + PRODUCT_NAME = DatadogSessionReplay; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 6133D1F42A6ED9E100384BEF /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplay; + PRODUCT_NAME = DatadogSessionReplay; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 6133D2052A6EDB7700384BEF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example iOS"; + }; + name = Debug; + }; + 6133D2062A6EDB7700384BEF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example iOS"; + }; + name = Release; + }; + 6133D2072A6EDB7700384BEF /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogSessionReplayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example iOS"; + }; + name = Integration; + }; + 61441C1424616DEC003D8BB8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + 61441C1524616DEC003D8BB8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; + 61441C1624616DEC003D8BB8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Integration; + }; + 618F9848265BC486009959F8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2EInstrumentationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2EInstrumentationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = E2E; + }; + name = Debug; + }; + 618F9849265BC486009959F8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2EInstrumentationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2EInstrumentationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = E2E; + }; + name = Release; + }; + 618F984A265BC486009959F8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 618F984C265BC53E009959F8 /* E2EInstrumentationTests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2EInstrumentationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2EInstrumentationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = E2E; + }; + name = Integration; + }; + 6199363C265BA95A009D7EA8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993641265BAD2D009D7EA8 /* E2E.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = TargetSupport/E2E/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadog.ios.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6199363D265BA95A009D7EA8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993641265BAD2D009D7EA8 /* E2E.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = TargetSupport/E2E/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadog.ios.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 6199363E265BA95A009D7EA8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993641265BAD2D009D7EA8 /* E2E.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = TargetSupport/E2E/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadog.ios.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 6199366D265BBEDC009D7EA8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993672265BC029009D7EA8 /* E2ETests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2ETests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2ETests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/E2E.app/E2E"; + }; + name = Debug; + }; + 6199366E265BBEDC009D7EA8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993672265BC029009D7EA8 /* E2ETests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2ETests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2ETests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/E2E.app/E2E"; + }; + name = Release; + }; + 6199366F265BBEDC009D7EA8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61993672265BC029009D7EA8 /* E2ETests.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/E2ETests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = Datadog.E2ETests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/E2E.app/E2E"; + }; + name = Integration; + }; + 61B7886525C180CB002675B5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 61B7886625C180CB002675B5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 61B7886725C180CB002675B5 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 61B7886825C180CB002675B5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 61B7886925C180CB002675B5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 61B7886A25C180CB002675B5 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 9E2FB28224476765001C9B7B /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; @@ -1863,147 +11438,1557 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Integration; + }; + 9E2FB28324476765001C9B7B /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 9E2FB28424476765001C9B7B /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + 9E2FB28524476765001C9B7B /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example iOS.app/Example iOS"; + }; + name = Integration; + }; + D207318B29A5226B00ECBF94 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = DatadogLogs; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D207318C29A5226B00ECBF94 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = DatadogLogs; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D207318D29A5226B00ECBF94 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + PRODUCT_NAME = DatadogLogs; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D207318E29A5226B00ECBF94 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D207318F29A5226B00ECBF94 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D207319029A5226B00ECBF94 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D20731B129A5279D00ECBF94 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_NAME = DatadogLogs; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + }; + name = Debug; + }; + D20731B229A5279D00ECBF94 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_NAME = DatadogLogs; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + }; + name = Release; + }; + D20731B329A5279D00ECBF94 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogs; + PRODUCT_NAME = DatadogLogs; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + }; + name = Integration; + }; + D23039A9298D513D001A1FA3 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D23039AA298D513D001A1FA3 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D23039AB298D513D001A1FA3 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D23F8E9629DDCD28001CFAE8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D23F8E9729DDCD28001CFAE8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + D23F8E9829DDCD28001CFAE8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D23F8ECA29DDCD38001CFAE8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D23F8ECB29DDCD38001CFAE8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + D23F8ECC29DDCD38001CFAE8 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D240684A27CE6C9E00C04F44 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + D240684B27CE6C9E00C04F44 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; + D240684C27CE6C9E00C04F44 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E1B082CB25641DF9002DB9D2 /* Example.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_MODULE_NAME = Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/Example/Example-Bridging-Header.h"; + VALIDATE_WORKSPACE = YES; + }; + name = Integration; + }; + D2579543298ABA65008A1BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D2579544298ABA65008A1BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D2579545298ABA65008A1BE5 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D2579588298ABB83008A1BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D2579589298ABB83008A1BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + D257958A298ABB83008A1BE5 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.TestUtilities; + PRODUCT_NAME = TestUtilities; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D25EE94329C4C3C400CE3839 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D25EE94429C4C3C400CE3839 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D25EE94529C4C3C400CE3839 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D25EE94629C4C3C400CE3839 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D25EE94729C4C3C400CE3839 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D25EE94829C4C3C400CE3839 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D29A9F4329DD84AB005C54A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - 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 = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG DD_SDK_DEVELOPMENT"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 61133B95242393DE00786299 /* Release */ = { + D29A9F4429DD84AB005C54A4 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - 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 = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D29A9F4529DD84AB005C54A4 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUM; + PRODUCT_NAME = DatadogRUM; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D29A9F4629DD84AB005C54A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D29A9F4729DD84AB005C54A4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D29A9F4829DD84AB005C54A4 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogRUMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D2A783FE29A534F9003B03BB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D2A783FF29A534F9003B03BB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D2A7840029A534F9003B03BB /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogLogsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Integration; + }; + D2C1A55729C4F2DF00946C31 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D2C1A55829C4F2DF00946C31 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + D2C1A55929C4F2DF00946C31 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTrace; + PRODUCT_NAME = DatadogTrace; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D2C1A57029C4F2E800946C31 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D2C1A57129C4F2E800946C31 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; - 61133B97242393DE00786299 /* Debug */ = { + D2C1A57229C4F2E800946C31 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogTraceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D2CB6ECE27C50EAE00A62B57 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = TargetSupport/Datadog/Info.plist; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "$(SRCROOT)/../Sources/Datadog/Datadog.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 61133B98242393DE00786299 /* Release */ = { + D2CB6ECF27C50EAE00A62B57 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = TargetSupport/Datadog/Info.plist; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "$(SRCROOT)/../Sources/Datadog/Datadog.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 61133B9A242393DE00786299 /* Debug */ = { + D2CB6ED027C50EAE00A62B57 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; + PRODUCT_NAME = "$(DD_SWIFT_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + }; + name = Integration; + }; + D2CB6F8C27C520D400A62B57 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; @@ -2016,18 +13001,17 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example tvOS.app/Example tvOS"; }; name = Debug; }; - 61133B9B242393DE00786299 /* Release */ = { + D2CB6F8D27C520D400A62B57 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; @@ -2040,22 +13024,48 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example tvOS.app/Example tvOS"; }; name = Release; }; - 61133C02242397DA00786299 /* Debug */ = { + D2CB6F8E27C520D400A62B57 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example tvOS.app/Example tvOS"; + }; + name = Integration; + }; + D2CB6FAD27C5217A00A62B57 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2064,22 +13074,28 @@ "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; - 61133C03242397DA00786299 /* Release */ = { + D2CB6FAE27C5217A00A62B57 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2088,310 +13104,477 @@ "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; - 61441C1424616DEC003D8BB8 /* Debug */ = { + D2CB6FAF27C5217A00A62B57 /* Integration */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/Example/Info.plist; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build/", + ); + INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; + PRODUCT_NAME = "$(DD_OBJC_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Integration; + }; + D2CB6FCE27C5348200A62B57 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; - 61441C1524616DEC003D8BB8 /* Release */ = { + D2CB6FCF27C5348200A62B57 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/Example/Info.plist; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; - 61441C1624616DEC003D8BB8 /* Integration */ = { + D2CB6FD027C5348200A62B57 /* Integration */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 615D9E2626048EAF006DC6D1 /* DatadogCrashReporting.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/Example/Info.plist; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../Carthage/Build", + ); + INFOPLIST_FILE = TargetSupport/DatadogCrashReporting/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReporting; + PRODUCT_NAME = "$(DD_CR_SDK_PRODUCT_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 3; }; name = Integration; }; - 61441C3224616F1D003D8BB8 /* Debug */ = { + D2CB6FE927C5352300A62B57 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = Example; + SDKROOT = appletvos; }; name = Debug; }; - 61441C3324616F1D003D8BB8 /* Release */ = { + D2CB6FEA27C5352300A62B57 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = Example; + SDKROOT = appletvos; }; name = Release; }; - 61441C3424616F1D003D8BB8 /* Integration */ = { + D2CB6FEB27C5352300A62B57 /* Integration */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; + baseConfigurationReference = 6170DC2B25C1883E003AED5C /* DatadogCrashReportingTests.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; + INFOPLIST_FILE = TargetSupport/DatadogCrashReportingTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogCrashReportingTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = Example; + SDKROOT = appletvos; }; name = Integration; }; - 61441C7124619FE4003D8BB8 /* Debug */ = { + D2DA2382298D57AA00C6C7E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; - 61441C7224619FE4003D8BB8 /* Release */ = { + D2DA2383298D57AA00C6C7E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; - 61441C7324619FE4003D8BB8 /* Integration */ = { + D2DA2384298D57AA00C6C7E6 /* Integration */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61569894256D0E9A00C6AADA /* Base.xcconfig */; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Datadog. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternal; + PRODUCT_NAME = DatadogInternal; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = 3; }; name = Integration; }; - 9E2FB28224476765001C9B7B /* Integration */ = { + D2DA2392298D588A00C6C7E6 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; + 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_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; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - 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 = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Integration; + name = Debug; }; - 9E2FB28324476765001C9B7B /* Integration */ = { + D2DA2393298D588A00C6C7E6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = TargetSupport/Datadog/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "$(SRCROOT)/../Sources/Datadog/Datadog.modulemap"; - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Datadog; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Integration; + name = Release; }; - 9E2FB28424476765001C9B7B /* Integration */ = { + D2DA2394298D588A00C6C7E6 /* Integration */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = TargetSupport/DatadogObjc/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogObjc; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Integration; }; - 9E2FB28524476765001C9B7B /* Integration */ = { + D2DA23C0298D59DC00C6C7E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + baseConfigurationReference = 61378BA72555329E00F28837 /* DatadogTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + D2DA23C1298D59DC00C6C7E6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + D2DA23C2298D59DC00C6C7E6 /* Integration */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.datadoghq.DatadogInternalTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = 3; }; name = Integration; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3CE11A1929F7BE0B00202522 /* Build configuration list for PBXNativeTarget "DatadogWebViewTracking iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3CE11A1329F7BE0B00202522 /* Debug */, + 3CE11A1429F7BE0B00202522 /* Release */, + 3CE11A1529F7BE0B00202522 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3CE11A1A29F7BE0B00202522 /* Build configuration list for PBXNativeTarget "DatadogWebViewTrackingTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3CE11A1629F7BE0B00202522 /* Debug */, + 3CE11A1729F7BE0B00202522 /* Release */, + 3CE11A1829F7BE0B00202522 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 61133B7C242393DE00786299 /* Build configuration list for PBXProject "Datadog" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2402,7 +13585,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61133B96242393DE00786299 /* Build configuration list for PBXNativeTarget "Datadog" */ = { + 61133B96242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogCore iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 61133B97242393DE00786299 /* Debug */, @@ -2412,7 +13595,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61133B99242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogTests" */ = { + 61133B99242393DE00786299 /* Build configuration list for PBXNativeTarget "DatadogCoreTests iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 61133B9A242393DE00786299 /* Debug */, @@ -2422,7 +13605,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61133C01242397DA00786299 /* Build configuration list for PBXNativeTarget "DatadogObjc" */ = { + 61133C01242397DA00786299 /* Build configuration list for PBXNativeTarget "DatadogObjc iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 61133C02242397DA00786299 /* Debug */, @@ -2432,7 +13615,27 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example" */ = { + 6133D1F12A6ED9E100384BEF /* Build configuration list for PBXNativeTarget "DatadogSessionReplay iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6133D1F22A6ED9E100384BEF /* Debug */, + 6133D1F32A6ED9E100384BEF /* Release */, + 6133D1F42A6ED9E100384BEF /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6133D2042A6EDB7700384BEF /* Build configuration list for PBXNativeTarget "DatadogSessionReplayTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6133D2052A6EDB7700384BEF /* Debug */, + 6133D2062A6EDB7700384BEF /* Release */, + 6133D2072A6EDB7700384BEF /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 61441C1424616DEC003D8BB8 /* Debug */, @@ -2442,32 +13645,319 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61441C3124616F1D003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */ = { + 618F9847265BC486009959F8 /* Build configuration list for PBXNativeTarget "E2EInstrumentationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 618F9848265BC486009959F8 /* Debug */, + 618F9849265BC486009959F8 /* Release */, + 618F984A265BC486009959F8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6199363F265BA95A009D7EA8 /* Build configuration list for PBXNativeTarget "E2E" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6199363C265BA95A009D7EA8 /* Debug */, + 6199363D265BA95A009D7EA8 /* Release */, + 6199363E265BA95A009D7EA8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6199366C265BBEDC009D7EA8 /* Build configuration list for PBXNativeTarget "E2ETests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6199366D265BBEDC009D7EA8 /* Debug */, + 6199366E265BBEDC009D7EA8 /* Release */, + 6199366F265BBEDC009D7EA8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61B7886B25C180CB002675B5 /* Build configuration list for PBXNativeTarget "DatadogCrashReporting iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 61B7886525C180CB002675B5 /* Debug */, + 61B7886625C180CB002675B5 /* Release */, + 61B7886725C180CB002675B5 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61B7886C25C180CB002675B5 /* Build configuration list for PBXNativeTarget "DatadogCrashReportingTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 61B7886825C180CB002675B5 /* Debug */, + 61B7886925C180CB002675B5 /* Release */, + 61B7886A25C180CB002675B5 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D207319129A5226B00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogs iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D207318B29A5226B00ECBF94 /* Debug */, + D207318C29A5226B00ECBF94 /* Release */, + D207318D29A5226B00ECBF94 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D207319229A5226B00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogsTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D207318E29A5226B00ECBF94 /* Debug */, + D207318F29A5226B00ECBF94 /* Release */, + D207319029A5226B00ECBF94 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D20731B029A5279D00ECBF94 /* Build configuration list for PBXNativeTarget "DatadogLogs tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D20731B129A5279D00ECBF94 /* Debug */, + D20731B229A5279D00ECBF94 /* Release */, + D20731B329A5279D00ECBF94 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D23039AC298D513D001A1FA3 /* Build configuration list for PBXNativeTarget "DatadogInternal iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D23039A9298D513D001A1FA3 /* Debug */, + D23039AA298D513D001A1FA3 /* Release */, + D23039AB298D513D001A1FA3 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D23F8E9529DDCD28001CFAE8 /* Build configuration list for PBXNativeTarget "DatadogRUM tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D23F8E9629DDCD28001CFAE8 /* Debug */, + D23F8E9729DDCD28001CFAE8 /* Release */, + D23F8E9829DDCD28001CFAE8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D23F8EC929DDCD38001CFAE8 /* Build configuration list for PBXNativeTarget "DatadogRUMTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D23F8ECA29DDCD38001CFAE8 /* Debug */, + D23F8ECB29DDCD38001CFAE8 /* Release */, + D23F8ECC29DDCD38001CFAE8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D240684927CE6C9E00C04F44 /* Build configuration list for PBXNativeTarget "Example tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D240684A27CE6C9E00C04F44 /* Debug */, + D240684B27CE6C9E00C04F44 /* Release */, + D240684C27CE6C9E00C04F44 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2579542298ABA65008A1BE5 /* Build configuration list for PBXNativeTarget "TestUtilities iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2579543298ABA65008A1BE5 /* Debug */, + D2579544298ABA65008A1BE5 /* Release */, + D2579545298ABA65008A1BE5 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2579587298ABB83008A1BE5 /* Build configuration list for PBXNativeTarget "TestUtilities tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2579588298ABB83008A1BE5 /* Debug */, + D2579589298ABB83008A1BE5 /* Release */, + D257958A298ABB83008A1BE5 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D25EE94929C4C3C400CE3839 /* Build configuration list for PBXNativeTarget "DatadogTrace iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D25EE94329C4C3C400CE3839 /* Debug */, + D25EE94429C4C3C400CE3839 /* Release */, + D25EE94529C4C3C400CE3839 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D25EE94A29C4C3C400CE3839 /* Build configuration list for PBXNativeTarget "DatadogTraceTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D25EE94629C4C3C400CE3839 /* Debug */, + D25EE94729C4C3C400CE3839 /* Release */, + D25EE94829C4C3C400CE3839 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D29A9F4929DD84AB005C54A4 /* Build configuration list for PBXNativeTarget "DatadogRUM iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D29A9F4329DD84AB005C54A4 /* Debug */, + D29A9F4429DD84AB005C54A4 /* Release */, + D29A9F4529DD84AB005C54A4 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D29A9F4A29DD84AB005C54A4 /* Build configuration list for PBXNativeTarget "DatadogRUMTests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D29A9F4629DD84AB005C54A4 /* Debug */, + D29A9F4729DD84AB005C54A4 /* Release */, + D29A9F4829DD84AB005C54A4 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2A783FD29A534F9003B03BB /* Build configuration list for PBXNativeTarget "DatadogLogsTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2A783FE29A534F9003B03BB /* Debug */, + D2A783FF29A534F9003B03BB /* Release */, + D2A7840029A534F9003B03BB /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2C1A55629C4F2DF00946C31 /* Build configuration list for PBXNativeTarget "DatadogTrace tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2C1A55729C4F2DF00946C31 /* Debug */, + D2C1A55829C4F2DF00946C31 /* Release */, + D2C1A55929C4F2DF00946C31 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2C1A56F29C4F2E800946C31 /* Build configuration list for PBXNativeTarget "DatadogTraceTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2C1A57029C4F2E800946C31 /* Debug */, + D2C1A57129C4F2E800946C31 /* Release */, + D2C1A57229C4F2E800946C31 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CB6ECD27C50EAE00A62B57 /* Build configuration list for PBXNativeTarget "DatadogCore tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CB6ECE27C50EAE00A62B57 /* Debug */, + D2CB6ECF27C50EAE00A62B57 /* Release */, + D2CB6ED027C50EAE00A62B57 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CB6F8B27C520D400A62B57 /* Build configuration list for PBXNativeTarget "DatadogCoreTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CB6F8C27C520D400A62B57 /* Debug */, + D2CB6F8D27C520D400A62B57 /* Release */, + D2CB6F8E27C520D400A62B57 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CB6FAC27C5217A00A62B57 /* Build configuration list for PBXNativeTarget "DatadogObjc tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CB6FAD27C5217A00A62B57 /* Debug */, + D2CB6FAE27C5217A00A62B57 /* Release */, + D2CB6FAF27C5217A00A62B57 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CB6FCD27C5348200A62B57 /* Build configuration list for PBXNativeTarget "DatadogCrashReporting tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CB6FCE27C5348200A62B57 /* Debug */, + D2CB6FCF27C5348200A62B57 /* Release */, + D2CB6FD027C5348200A62B57 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CB6FE827C5352300A62B57 /* Build configuration list for PBXNativeTarget "DatadogCrashReportingTests tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CB6FE927C5352300A62B57 /* Debug */, + D2CB6FEA27C5352300A62B57 /* Release */, + D2CB6FEB27C5352300A62B57 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2DA2381298D57AA00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternal tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2DA2382298D57AA00C6C7E6 /* Debug */, + D2DA2383298D57AA00C6C7E6 /* Release */, + D2DA2384298D57AA00C6C7E6 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2DA2391298D588A00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternalTests iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - 61441C3224616F1D003D8BB8 /* Debug */, - 61441C3324616F1D003D8BB8 /* Release */, - 61441C3424616F1D003D8BB8 /* Integration */, + D2DA2392298D588A00C6C7E6 /* Debug */, + D2DA2393298D588A00C6C7E6 /* Release */, + D2DA2394298D588A00C6C7E6 /* Integration */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */ = { + D2DA23BF298D59DC00C6C7E6 /* Build configuration list for PBXNativeTarget "DatadogInternalTests tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( - 61441C7124619FE4003D8BB8 /* Debug */, - 61441C7224619FE4003D8BB8 /* Release */, - 61441C7324619FE4003D8BB8 /* Integration */, + D2DA23C0298D59DC00C6C7E6 /* Debug */, + D2DA23C1298D59DC00C6C7E6 /* Release */, + D2DA23C2298D59DC00C6C7E6 /* Integration */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/DataDog/dd-sdk-swift-testing.git"; + requirement = { + kind = exactVersion; + version = "2.5.3-beta1"; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ - 61441C43246174CE003D8BB8 /* HTTPServerMock */ = { + 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */ = { + isa = XCSwiftPackageProductDependency; + package = 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */; + productName = DatadogSDKTesting; + }; + 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */ = { isa = XCSwiftPackageProductDependency; - productName = HTTPServerMock; + package = 3CDA3F6C2BCD8429005D2C13 /* XCRemoteSwiftPackageReference "dd-sdk-swift-testing" */; + productName = DatadogSDKTesting; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist index 33953da047..1c6313aac5 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist @@ -57,7 +57,7 @@ 30 - testWrittingLogsOnDisc() + testWritingLogsOnDisc() com.apple.XCTPerformanceMetric_WallClockTime diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist new file mode 100644 index 0000000000..af86bb6f65 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist @@ -0,0 +1,141 @@ + + + + + classNames + + LoggingBenchmarkTests + + testCreatingOneLog() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.5537e-05 + baselineIntegrationDisplayName + 7 Jul 2020 at 10:28:47 + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithAttributes() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.6391e-05 + baselineIntegrationDisplayName + 7 Jul 2020 at 10:28:47 + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithTags() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.2e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + LoggingStorageBenchmarkTests + + testReadingLogsFromDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00151 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testWritingLogsOnDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00376 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + TracingBenchmarkTests + + testCreatingAndEndingOneSpan() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 5.81e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneSpanWithBaggageItems() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.89e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneSpanWithTags() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.48e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + TracingStorageBenchmarkTests + + testReadingSpansFromDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00139 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testWritingSpansOnDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00352 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist index d7d73d8ce2..91bcc1a3ea 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist @@ -16,6 +16,18 @@ com.apple.platform.iphoneos + FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52 + + targetArchitecture + arm64e + targetDevice + + modelCode + iPhone12,1 + platformIdentifier + com.apple.platform.iphoneos + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme deleted file mode 100644 index 40e0b2299f..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme deleted file mode 100644 index dd85729c19..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogBenchmarkTests.xcscheme +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme new file mode 100644 index 0000000000..eaca1e4c15 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme new file mode 100644 index 0000000000..fc53609cdd --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme new file mode 100644 index 0000000000..853f578441 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting iOS.xcscheme @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme new file mode 100644 index 0000000000..417294662d --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCrashReporting tvOS.xcscheme @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme deleted file mode 100644 index 8dc1b17f7d..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme new file mode 100644 index 0000000000..a4422a9c7e --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal iOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme new file mode 100644 index 0000000000..251f49560e --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogInternal tvOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme new file mode 100644 index 0000000000..96304d7948 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs iOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme new file mode 100644 index 0000000000..bb2fb24be0 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogLogs tvOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme new file mode 100644 index 0000000000..3749d2bd23 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc iOS.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc tvOS.xcscheme new file mode 100644 index 0000000000..adf3190572 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc tvOS.xcscheme @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme deleted file mode 100644 index e5ae68ed69..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM iOS.xcscheme new file mode 100644 index 0000000000..0911175fa7 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM iOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme new file mode 100644 index 0000000000..13de3f1975 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogRUM tvOS.xcscheme @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme new file mode 100644 index 0000000000..07a31a4557 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogSessionReplay iOS.xcscheme @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme new file mode 100644 index 0000000000..05898dd9fd --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace iOS.xcscheme @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme new file mode 100644 index 0000000000..0ba9e725e5 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogTrace tvOS.xcscheme @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme new file mode 100644 index 0000000000..a5fee88576 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogWebViewTracking iOS.xcscheme @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2E.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2E.xcscheme new file mode 100644 index 0000000000..f4502cbc6c --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2E.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2EInstrumentationTests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2EInstrumentationTests.xcscheme new file mode 100644 index 0000000000..c34a75f359 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2EInstrumentationTests.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2ETests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2ETests.xcscheme new file mode 100644 index 0000000000..6fc2c8f32c --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/E2ETests.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme new file mode 100644 index 0000000000..bc62ca4664 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example iOS.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example tvOS.xcscheme new file mode 100644 index 0000000000..619b75b1ec --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example tvOS.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme deleted file mode 100644 index 0117dc076d..0000000000 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities iOS.xcscheme new file mode 100644 index 0000000000..518eec96d1 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities iOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities tvOS.xcscheme new file mode 100644 index 0000000000..4ba8471e85 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/TestUtilities tvOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/E2E/Assets.xcassets/AccentColor.colorset/Contents.json b/Datadog/E2E/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Datadog/E2E/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Datadog/E2E/Assets.xcassets/AppIcon.appiconset/Contents.json b/Datadog/E2E/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Datadog/E2E/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Datadog/E2E/Assets.xcassets/Contents.json b/Datadog/E2E/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Datadog/E2E/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shopist/Shopist/Resources/LaunchScreen.storyboard b/Datadog/E2E/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Shopist/Shopist/Resources/LaunchScreen.storyboard rename to Datadog/E2E/Base.lproj/LaunchScreen.storyboard diff --git a/Datadog/E2E/E2EAppDelegate.swift b/Datadog/E2E/E2EAppDelegate.swift new file mode 100644 index 0000000000..147b3c7efd --- /dev/null +++ b/Datadog/E2E/E2EAppDelegate.swift @@ -0,0 +1,16 @@ +/* +* 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 + +@UIApplicationMain +internal class E2EAppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + E2EConfig.check() + + return true + } +} diff --git a/Datadog/E2E/E2EConfig.swift b/Datadog/E2E/E2EConfig.swift new file mode 100644 index 0000000000..39d6e1b340 --- /dev/null +++ b/Datadog/E2E/E2EConfig.swift @@ -0,0 +1,62 @@ +/* + * 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 + +internal class E2EConfig { + private struct InfoPlistKey { + static let clientToken = "E2EDatadogClientToken" + static let rumApplicationID = "E2ERUMApplicationID" + static let isRunningOnCI = "IsRunningOnCI" + } + + private static var bundle: Bundle { Bundle(for: E2EConfig.self) } + + // MARK: - Info.plist + + /// Reads Datadog client token authorizing data for 'Mobile - Instrumentation' org. + static func readClientToken() -> String { + guard let clientToken = bundle.infoDictionary?[InfoPlistKey.clientToken] as? String, !clientToken.isEmpty else { + fatalError( + """ + ✋⛔️ Cannot read `\(InfoPlistKey.clientToken)` from `Info.plist` dictionary. + Update `xcconfigs/Datadog.xcconfig` with your own client token obtained on datadoghq.com. + You might need to run `Product > Clean Build Folder` before retrying. + """ + ) + } + return clientToken + } + + /// Reads RUM Application ID authorizing data for 'Mobile - Instrumentation' org. + static func readRUMApplicationID() -> String { + guard let rumApplicationID = bundle.infoDictionary?[InfoPlistKey.rumApplicationID] as? String, !rumApplicationID.isEmpty else { + fatalError( + """ + ✋⛔️ Cannot read `\(InfoPlistKey.rumApplicationID)` from `Info.plist` dictionary. + Update `xcconfigs/Datadog.xcconfig` with your own RUM application id obtained on datadoghq.com. + You might need to run `Product > Clean Build Folder` before retrying. + """ + ) + } + return rumApplicationID + } + + /// Reads Datadog ENV for tagging events. Returns `debug` for local builds and `instrumentation` for CI. + /// This way local debug data is excluded from monitors installed in 'Mobile - Instrumentation' org. + static func readEnv() -> String { + let isCI = bundle.infoDictionary?[InfoPlistKey.isRunningOnCI] as? String + return isCI == "true" ? "instrumentation" : "debug" + } + + /// Checks the ENV configuration consistency. + /// TODO: RUMM-1249 remove this method when we have both manual and instrumented API tests using client token and RUM app ID. + static func check() { + _ = readClientToken() + _ = readRUMApplicationID() + print("⚙️ Using DD ENV: '\(readEnv())'") + } +} diff --git a/Datadog/E2EInstrumentationTests/E2EInstrumentationTests.swift b/Datadog/E2EInstrumentationTests/E2EInstrumentationTests.swift new file mode 100644 index 0000000000..fe19b84a9f --- /dev/null +++ b/Datadog/E2EInstrumentationTests/E2EInstrumentationTests.swift @@ -0,0 +1,15 @@ +/* +* 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 + +class E2EInstrumentationTests: XCTestCase { + func testBasicInstrumentation() throws { + let app = XCUIApplication() + app.launch() + app.terminate() + } +} diff --git a/Datadog/E2ETests/E2ETests.swift b/Datadog/E2ETests/E2ETests.swift new file mode 100644 index 0000000000..71557a0eca --- /dev/null +++ b/Datadog/E2ETests/E2ETests.swift @@ -0,0 +1,83 @@ +/* +* 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 +import DatadogLogs +import DatadogTrace +import DatadogRUM + +@testable import DatadogCore + +/// A base class for all E2E test cases. +class E2ETests: XCTestCase { + /// If enabled, the SDK will not be initialized before each test. + var skipSDKInitialization = false + + // MARK: - Before & After Each Test + + override func setUp() { + super.setUp() + deleteAllSDKData() + if !skipSDKInitialization { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + Trace.enable() + RUM.enable(with: .e2e) + } + } + + override func tearDown() { + sendAllDataAndDeinitializeSDK() + super.tearDown() + } + + // MARK: - Common Monitors + + /// - common performance monitor - measures `Datadog.initialize(...)` performance: + /// ```apm + /// $feature = core + /// $monitor_id = sdk_initialize_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - sdk_initialize: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:sdk_initialize,service:com.datadog.ios.nightly} > 0.016" + /// $monitor_threshold = 0.016 + /// ``` + + // MARK: - Measuring Performance with APM + + /// Measures time of execution for given `block` - sends it as a `"perf_measure"` `Span` with a given resource name. + @discardableResult + func measure(resourceName: String, _ block: () -> T) -> T { + let start = Date() + let result = block() + let stop = Date() + + let performanceSpan = Tracer.shared().startRootSpan(operationName: "perf_measure", startTime: start) + performanceSpan.setTag(key: SpanTags.resource, value: resourceName) + performanceSpan.finish(at: stop) + + return result + } + + // MARK: - SDK Lifecycle + + /// Sends all collected data and deinitializes the SDK. It is executed synchronously. + private func sendAllDataAndDeinitializeSDK() { + Datadog.flushAndDeinitialize() + } + + // MARK: - Helpers + + /// Deletes persisted data for all SDK features. Ensures clean start for each test. + private func deleteAllSDKData() { + let core = CoreRegistry.default as? DatadogCore + core?.stores.values.forEach { $0.storage.clearAllData() } + } +} diff --git a/Datadog/E2ETests/E2EUtils.swift b/Datadog/E2ETests/E2EUtils.swift new file mode 100644 index 0000000000..3644aad260 --- /dev/null +++ b/Datadog/E2ETests/E2EUtils.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// Collection of utilities for creating values for facets configured in "Mobile - Integration" org. +struct DD { + /// Collection of performance span names for measuring time performance of different APIs. + /// + /// There is a performance monitor defined for each span name from this collection. + struct PerfSpanName { + /// Builds the span name by extracting it from the caller method + /// name (it removes `test_` prefix, `()` suffix and converts the name to lowercase). + static func fromCurrentMethodName(functionName: StaticString = #function) -> String { + return testMethodName(functionName: functionName) + } + + // MARK: - Common + + static let sdkInitialize = "sdk_initialize" + static let setTrackingConsent = "sdk_set_tracking_consent" + + // MARK: - Logging-specific + + static let loggerInitialize = "logs_logger_initialize" + + // MARK: - RUM-specific + + static let rumAttributeAddAttribute = "rum_globalrum_add_attribute" + static let rumAttributeRemoveAttribute = "rum_globalrum_remove_attribute" + } + + // MARK: - Special Attributes + + /// Special attribute added to events. + /// + /// There is a facet created for each produced **attribute key**. + /// Some **attribute values** contain fixed part, which is additionally asserted in monitor. + /// + /// We only test `String`, `Int`, `Double` and `Bool` attributes as these are the only ones available for facets at Datadog. + struct SpecialAttribute { + let key: String + let value: Encodable + + fileprivate init(key: String, value: Encodable) { + self.key = key + self.value = value + } + } + + static func specialStringAttribute() -> SpecialAttribute { + let prefix = "customAttribute" // asserted in monitors (`@test_special_string_attribute:customAttribute*`) + return .init(key: "test_special_string_attribute", value: prefix + .mockRandom()) + } + + static func specialIntAttribute() -> SpecialAttribute { + let min: Int = 11 // asserted in monitors (`@test_special_int_attribute:>10`) + return .init(key: "test_special_int_attribute", value: Int.mockRandom(min: min, max: .max)) + } + + static func specialDoubleAttribute() -> SpecialAttribute { + let min: Double = 11.0 // asserted in monitors (`@test_special_double_attribute:>10.0`) + return .init(key: "test_special_double_attribute", value: Double.mockRandom(min: min, max: .greatestFiniteMagnitude)) + } + + static func specialBoolAttribute() -> SpecialAttribute { + let value: Bool = .random() // asserted in monitors (`@test_special_bool_attribute:(true OR false)`) + return .init(key: "test_special_bool_attribute", value: value) + } + + // MARK: - Special Tags + + /// Special tag added to events. + /// + /// There is a facet created for each produced **tag name**. + /// **Tag values** contain fixed part, which is additionally asserted in monitor. + struct SpecialTag { + let key: String + let value: String + + fileprivate init(key: String, value: String) { + self.key = key + self.value = value + } + } + + static func specialTag() -> SpecialTag { + let prefix = "customtag" // asserted in monitors (`@test_special_tag:customtag*`) + return .init(key: "test_special_tag", value: prefix + .mockRandom()) + } + + // MARK: - Logging-specific Attributes + + /// Attributes added to each log event. + /// + /// Each attributes has a facet used in monitor to assert that events are actually delivered. + static func logAttributes(functionName: StaticString = #function) -> [String: Encodable] { + return [ + "test_method_name": testMethodName(functionName: functionName) + ] + } +} + +/// Removes `test_` prefix, `()` suffix and converts the `functionName` to lowercase. +/// +/// e.g. if called with `test_logs_logger_debug_log()`, it will return `logs_logger_debug_log`. +private func testMethodName(functionName: StaticString = #function) -> String { + var name = "\(functionName)" + precondition(name.hasPrefix("test_"), "Cannot read `testMethodName` from: \(name) - it must have 'test_' prefix.") + precondition(name.hasSuffix("()"), "Cannot read `testMethodName` from: \(name) - it must have '()' suffix.") + name.removeFirst(("test_".count)) + name.removeLast("()".count) + return name.lowercased() +} diff --git a/Datadog/E2ETests/Helpers/DatadogE2EHelpers.swift b/Datadog/E2ETests/Helpers/DatadogE2EHelpers.swift new file mode 100644 index 0000000000..db48bcd78d --- /dev/null +++ b/Datadog/E2ETests/Helpers/DatadogE2EHelpers.swift @@ -0,0 +1,16 @@ +/* + * 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 DatadogCore + +extension Datadog.Configuration { + static var e2e: Self { + .init( + clientToken: E2EConfig.readClientToken(), + env: E2EConfig.readEnv() + ) + } +} diff --git a/Datadog/E2ETests/Helpers/LoggingE2EHelpers.swift b/Datadog/E2ETests/Helpers/LoggingE2EHelpers.swift new file mode 100644 index 0000000000..08ad3a9723 --- /dev/null +++ b/Datadog/E2ETests/Helpers/LoggingE2EHelpers.swift @@ -0,0 +1,40 @@ +/* + * 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 DatadogLogs +import TestUtilities + +extension LoggerProtocol { + func sendRandomLog(with attributes: [String: Encodable]) { + let message: String = .mockRandom() + let error: Error? = Bool.random() ? ErrorMock(.mockRandom()) : nil + + // swiftlint:disable opening_brace + let allMethods = [ + { self.debug(message, error: error, attributes: attributes) }, + { self.info(message, error: error, attributes: attributes) }, + { self.notice(message, error: error, attributes: attributes) }, + { self.warn(message, error: error, attributes: attributes) }, + { self.error(message, error: error, attributes: attributes) }, + { self.critical(message, error: error, attributes: attributes) }, + ] + // swiftlint:enable opening_brace + + let randomMethod = allMethods.randomElement()! + randomMethod() + } +} + +extension Logger.Configuration.ConsoleLogFormat { + static func random() -> Logger.Configuration.ConsoleLogFormat { + let allFormats: [Logger.Configuration.ConsoleLogFormat] = [ + .short, + .shortWith(prefix: .mockRandom()) + ] + + return allFormats.randomElement()! + } +} diff --git a/Datadog/E2ETests/Helpers/RUME2EHelpers.swift b/Datadog/E2ETests/Helpers/RUME2EHelpers.swift new file mode 100644 index 0000000000..6352b28149 --- /dev/null +++ b/Datadog/E2ETests/Helpers/RUME2EHelpers.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. + */ + +@testable import DatadogRUM + +extension RUMMonitorProtocol { + var dd: Monitor { self as! Monitor } +} + +extension RUM.Configuration { + static var e2e: Self { + .init( + applicationID: E2EConfig.readRUMApplicationID(), + telemetrySampleRate: 100 + ) + } +} diff --git a/Datadog/E2ETests/Logging/LoggerConfigurationTests.swift b/Datadog/E2ETests/Logging/LoggerConfigurationTests.swift new file mode 100644 index 0000000000..6502ba206a --- /dev/null +++ b/Datadog/E2ETests/Logging/LoggerConfigurationTests.swift @@ -0,0 +1,250 @@ +/* + * 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 DatadogCore +import DatadogLogs +import DatadogRUM +import DatadogTrace + +class LoggerConfigurationTests: E2ETests { + private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + + override func tearDown() { + logger = nil + super.tearDown() + } + + // MARK: - Common Monitors + + /// - common performance monitor - measures `Logger.create()` performance: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_initialize_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_initialize: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:logs_logger_initialize,service:com.datadog.ios.nightly} > 0.016" + /// $monitor_threshold = 0.016 + /// ``` + + // MARK: - Enabling Options + + /// - api-surface: Logger.Configuration(service: String) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_set_service_name_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_set_service_name: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly.custom @test_method_name:logs_logger_builder_set_service_name\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_set_SERVICE_NAME() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(service: "com.datadog.ios.nightly.custom")) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(name: String) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_set_logger_name_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_set_logger_name: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_set_logger_name @logger.name:custom_logger_name\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_set_LOGGER_NAME() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(name: "custom_logger_name")) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(networkInfoEnabled: Bool) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_send_network_info_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_send_network_info_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_send_network_info_enabled @network.client.reachability:*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_SEND_NETWORK_INFO_enabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(networkInfoEnabled: true)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(networkInfoEnabled: Bool) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_send_network_info_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_send_network_info_disabled: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_send_network_info_disabled @network.client.reachability:*\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_logger_builder_SEND_NETWORK_INFO_disabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(networkInfoEnabled: false)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Choosing Logs Output + + /// - api-surface: Logger.Configuration(remoteSampleRate: Float) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_send_logs_to_datadog_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_send_logs_to_datadog_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_send_logs_to_datadog_enabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_SEND_LOGS_TO_DATADOG_enabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(remoteSampleRate: 100)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(remoteSampleRate: Float) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_send_logs_to_datadog_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_send_logs_to_datadog_disabled: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_send_logs_to_datadog_disabled\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_logger_builder_SEND_LOGS_TO_DATADOG_disabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(remoteSampleRate: 0)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(consoleLogFormat: ConsoleLogFormat) -> Builder + /// + /// - data monitor - as long as sending logs to Datadog is enabled (which is default), this this monitor should receive data: + /// ```logs + /// $monitor_id = logs_logger_builder_print_logs_to_console_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_print_logs_to_console_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_print_logs_to_console_enabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_PRINT_LOGS_TO_CONSOLE_enabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(consoleLogFormat: .random())) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.Configuration(consoleLogFormat: ConsoleLogFormat) -> Builder + /// + /// - data monitor - as long as sending logs to Datadog is enabled (which is default), this this monitor should receive data: + /// ```logs + /// $monitor_id = logs_logger_builder_print_logs_to_console_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_print_logs_to_console_disabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_print_logs_to_console_disabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_PRINT_LOGS_TO_CONSOLE_disabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(consoleLogFormat: nil)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Bundling With Other Features + + /// - api-surface: Logger.Configuration(bundleWithRumEnabled: Bool) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_bundle_with_rum_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_bundle_with_rum_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_bundle_with_rum_enabled @application_id:* @session_id:*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_BUNDLE_WITH_RUM_enabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(bundleWithRumEnabled: true)) + } + + let viewKey: String = .mockRandom() + RUMMonitor.shared().startView(key: viewKey) + logger.sendRandomLog(with: DD.logAttributes()) + RUMMonitor.shared().stopView(key: viewKey) + } + + /// - api-surface: Logger.Configuration(bundleWithRumEnabled: Bool) -> Builder + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_builder_bundle_with_rum_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_bundle_with_rum_disabled: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_bundle_with_rum_disabled @application_id:* @session_id:* view.id:*\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_logger_builder_BUNDLE_WITH_RUM_disabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(bundleWithRumEnabled: false)) + } + + logger.sendRandomLog(with: DD.logAttributes()) + + let viewKey: String = .mockRandom() + RUMMonitor.shared().startView(key: viewKey) + logger.sendRandomLog(with: DD.logAttributes()) + RUMMonitor.shared().stopView(key: viewKey) + } + + /// - api-surface: Logger.Configuration(bundleWithTraceEnabled: Bool) -> Builder + /// + /// - data monitor - unfortunately we can't assert any APM trait in this monitor, so we just check if the data comes in: + /// ```logs + /// $monitor_id = logs_logger_builder_bundle_with_trace_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_bundle_with_trace_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_bundle_with_trace_enabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_BUNDLE_WITH_TRACE_enabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(bundleWithTraceEnabled: true)) + } + + let activeSpan = Tracer.shared() + .startRootSpan(operationName: .mockRandom()) + .setActive() + logger.sendRandomLog(with: DD.logAttributes()) + activeSpan.finish() + } + + /// - api-surface: Logger.Configuration(bundleWithTraceEnabled: Bool) -> Builder + /// + /// - data monitor - unfortunately we can't assert any APM trait in this monitor, so we just check if the data comes in: + /// ```logs + /// $monitor_id = logs_logger_builder_bundle_with_trace_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_builder_bundle_with_trace_disabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_builder_bundle_with_trace_disabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_logger_builder_BUNDLE_WITH_TRACE_disabled() { + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create(with: Logger.Configuration(bundleWithTraceEnabled: false)) + } + + let activeSpan = Tracer.shared() + .startRootSpan(operationName: .mockRandom()) + .setActive() + logger.sendRandomLog(with: DD.logAttributes()) + activeSpan.finish() + } +} diff --git a/Datadog/E2ETests/Logging/LoggerE2ETests.swift b/Datadog/E2ETests/Logging/LoggerE2ETests.swift new file mode 100644 index 0000000000..e6faf31031 --- /dev/null +++ b/Datadog/E2ETests/Logging/LoggerE2ETests.swift @@ -0,0 +1,541 @@ +/* + * 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 TestUtilities +import DatadogCore +import DatadogLogs + +class LoggerE2ETests: E2ETests { + private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + logger = Logger.create() + } + + override func tearDown() { + logger = nil + super.tearDown() + } + + // MARK: - Logging Method + + /// - api-surface: Logger.debug(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_debug_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_debug_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_debug_log status:debug\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_debug_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_debug_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_debug_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_DEBUG_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.debug(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.debug(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_debug_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_debug_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_debug_log_with_error status:debug\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_debug_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_debug_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_debug_log_with_error*,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_DEBUG_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.debug(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.info(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_info_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_info_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_info_log status:info\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_info_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_info_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_info_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_INFO_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.info(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.info(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_info_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_info_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_info_log_with_error status:info\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_info_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_info_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_info_log_with_error,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_INFO_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.info(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.notice(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_notice_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_notice_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_notice_log status:notice\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_notice_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_notice_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_notice_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_NOTICE_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.notice(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.notice(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_notice_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_notice_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_notice_log_with_error status:notice\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_notice_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_notice_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_notice_log_with_error,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_NOTICE_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.notice(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.warn(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_warn_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_warn_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_warn_log status:warn\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_warn_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_warn_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_warn_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_WARN_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.warn(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.warn(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_warn_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_warn_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_warn_log_with_error status:warn\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_warn_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_warn_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_warn_log_with_error,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_WARN_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.warn(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.error(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_error_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_error_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_error_log status:error\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_error_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_error_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_error_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_ERROR_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.error(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.error(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_error_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_error_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_error_log_with_error status:error\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_error_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_error_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_error_log_with_error,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_ERROR_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.error(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.critical(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_critical_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_critical_log: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_critical_log status:critical\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_critical_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_critical_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_critical_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_CRITICAL_log() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.critical(.mockRandom(), attributes: DD.logAttributes()) + } + } + + /// - api-surface: Logger.critical(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_critical_log_with_error_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_critical_log_with_error: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_critical_log_with_error status:critical\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_critical_log_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_critical_log_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_critical_log_with_error,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_CRITICAL_log_with_error() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.critical(.mockRandom(), error: ErrorMock(.mockRandom()), attributes: DD.logAttributes()) + } + } + + // MARK: - Adding Attributes + + /// - api-surface: Logger.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_string_attribute_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_string_attribute: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_string_attribute @test_special_string_attribute:customAttribute*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_string_attribute_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_string_attribute: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_string_attribute,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_STRING_attribute() { + let attribute = DD.specialStringAttribute() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.addAttribute(forKey: attribute.key, value: attribute.value) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_int_attribute_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_int_attribute: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_int_attribute @test_special_int_attribute:>10\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_int_attribute_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_int_attribute: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_int_attribute,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_INT_attribute() { + let attribute = DD.specialIntAttribute() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.addAttribute(forKey: attribute.key, value: attribute.value) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_double_attribute_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_double_attribute: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_double_attribute @test_special_double_attribute:>10\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_double_attribute_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_double_attribute: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_double_attribute,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_DOUBLE_attribute() { + let attribute = DD.specialDoubleAttribute() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.addAttribute(forKey: attribute.key, value: attribute.value) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_bool_attribute_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_bool_attribute: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_bool_attribute @test_special_bool_attribute:(true OR false)\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_bool_attribute_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_bool_attribute: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_bool_attribute,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_BOOL_attribute() { + let attribute = DD.specialBoolAttribute() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.addAttribute(forKey: attribute.key, value: attribute.value) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Removing Attributes + + /// - api-surface: Logger.removeAttribute(forKey key: AttributeKey) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_remove_attribute_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_remove_attribute: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_remove_attribute -@test_special_string_attribute:* -@test_special_int_attribute:* -@test_special_double_attribute:* -@test_special_bool_attribute:*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_remove_attribute_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_remove_attribute: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_remove_attribute,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_remove_attribute() { + let possibleAttributes = [ + DD.specialStringAttribute(), + DD.specialIntAttribute(), + DD.specialDoubleAttribute(), + DD.specialBoolAttribute() + ] + let randomAttribute = possibleAttributes.randomElement()! + + logger.addAttribute(forKey: randomAttribute.key, value: randomAttribute.value) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.removeAttribute(forKey: randomAttribute.key) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Adding Tags + + /// - api-surface: Logger.addTag(withKey key: String, value: String) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_tag_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_tag: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_tag test_special_tag:customtag*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_tag_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_tag: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_tag,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_tag() { + let tag = DD.specialTag() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.addTag(withKey: tag.key, value: tag.value) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.add(tag: String) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_add_already_formatted_tag_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_add_already_formatted_tag: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_add_already_formatted_tag test_special_tag:customtag*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_add_already_formatted_tag_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_add_already_formatted_tag: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:logs_logger_add_already_formatted_tag,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_logs_logger_add_already_formatted_tag() { + let tag = DD.specialTag() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.add(tag: "\(tag.key):\(tag.value)") + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Removing Tags + + /// - api-surface: Logger.removeTag(withKey key: String) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_remove_tag_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_remove_tag: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_remove_tag -test_special_tag:*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_remove_tag_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_remove_tag: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,service:com.datadog.ios.nightly,resource_name:logs_logger_remove_tag} > 0.024" + /// ``` + func test_logs_logger_remove_tag() { + let tag = DD.specialTag() + logger.addTag(withKey: tag.key, value: tag.value) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.removeTag(withKey: tag.key) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logger.remove(tag: String) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_logger_remove_already_formatted_tag_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_logger_remove_already_formatted_tag: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_logger_remove_already_formatted_tag -test_special_tag:*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = logs + /// $monitor_id = logs_logger_remove_already_formatted_tag_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - logs_logger_remove_already_formatted_tag: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,service:com.datadog.ios.nightly,resource_name:logs_logger_remove_already_formatted_tag} > 0.024" + /// ``` + func test_logs_logger_remove_already_formatted_tag() { + let tag = DD.specialTag() + logger.add(tag: "\(tag.key):\(tag.value)") + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + logger.remove(tag: "\(tag.key):\(tag.value)") + } + + logger.sendRandomLog(with: DD.logAttributes()) + } +} diff --git a/Datadog/E2ETests/Logging/LogsConfigurationE2ETests.swift b/Datadog/E2ETests/Logging/LogsConfigurationE2ETests.swift new file mode 100644 index 0000000000..f2047b1864 --- /dev/null +++ b/Datadog/E2ETests/Logging/LogsConfigurationE2ETests.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 DatadogCore +import DatadogLogs +import DatadogRUM +import DatadogTrace +import DatadogCrashReporting + +class LogsConfigurationE2ETests: E2ETests { + private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + skipSDKInitialization = true // we will initialize it in each test + super.setUp() + } + + override func tearDown() { + logger = nil + super.tearDown() + } + + /// - api-surface: Logs.enable() + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_config_feature_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_feature_enabled: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_feature_enabled\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_config_feature_enabled() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + Trace.enable() + RUM.enable(with: .e2e) + CrashReporting.enable() + } + + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Logs.enable() + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_feature_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_feature_disabled: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_feature_disabled\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_feature_disabled() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Trace.enable() + RUM.enable(with: .e2e) + CrashReporting.enable() + } + + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + + logger.sendRandomLog(with: DD.logAttributes()) + } +} diff --git a/Datadog/E2ETests/Logging/LogsTrackingConsentE2ETests.swift b/Datadog/E2ETests/Logging/LogsTrackingConsentE2ETests.swift new file mode 100644 index 0000000000..312241654f --- /dev/null +++ b/Datadog/E2ETests/Logging/LogsTrackingConsentE2ETests.swift @@ -0,0 +1,285 @@ +/* + * 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 DatadogCore +import DatadogLogs + +class LogsTrackingConsentE2ETests: E2ETests { + private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + skipSDKInitialization = true // we will initialize it in each test + super.setUp() + } + + override func tearDown() { + logger = nil + super.tearDown() + } + + // MARK: - Common Monitors + + /// - common performance monitor - measures `Datadog.set(trackingConsent:)` performance: + /// ```apm + /// $feature = logs + /// $monitor_id = sdk_set_tracking_consent_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - sdk_set_tracking_consent: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:sdk_set_tracking_consent,service:com.datadog.ios.nightly} > 0.016" + /// $monitor_threshold = 0.016 + /// ``` + + // MARK: - Starting With a Consent + + /// - api-surface: Datadog.initialize(with : Configuration, trackingConsent: TrackingConsent) + /// - api-surface: TrackingConsent.granted + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_config_consent_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_granted: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_granted\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_config_consent_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.initialize(with : Configuration, trackingConsent: TrackingConsent) + /// - api-surface: TrackingConsent.notGranted + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_not_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_not_granted: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_not_granted\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_NOT_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.initialize(with : Configuration, trackingConsent: TrackingConsent) + /// - api-surface: TrackingConsent.pending + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_pending_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_pending: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_pending\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_PENDING() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + logger.sendRandomLog(with: DD.logAttributes()) + } + + // MARK: - Changing Consent + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_granted_to_not_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_granted_to_not_granted: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_granted_to_not_granted\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_GRANTED_to_NOT_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .notGranted) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_granted_to_pending_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_granted_to_pending: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_granted_to_pending\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_GRANTED_to_PENDING() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .pending) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_config_consent_not_granted_to_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_not_granted_to_granted: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_not_granted_to_granted\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_config_consent_NOT_GRANTED_to_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .granted) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_not_granted_to_pending_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_not_granted_to_pending: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_not_granted_to_pending\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_NOT_GRANTED_to_PENDING() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .pending) + } + + logger.sendRandomLog(with: DD.logAttributes()) + } + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```logs + /// $monitor_id = logs_config_consent_pending_to_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_pending_to_granted: number of logs is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_pending_to_granted\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_logs_config_consent_PENDING_to_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + + logger.sendRandomLog(with: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .granted) + } + } + + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor - we assert that no data is delivered in this monitor: + /// ```logs + /// $monitor_id = logs_config_consent_pending_to_not_granted_data + /// $monitor_name = "[RUM] [iOS] Nightly - logs_config_consent_pending_to_not_granted: number of logs is above expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:logs_config_consent_pending_to_not_granted\").index(\"*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// $notify_no_data = false + /// ``` + func test_logs_config_consent_PENDING_to_NOT_GRANTED() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + Logs.enable() + } + measure(resourceName: DD.PerfSpanName.loggerInitialize) { + logger = Logger.create() + } + + logger.sendRandomLog(with: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .notGranted) + } + } +} diff --git a/Datadog/E2ETests/NTP/KronosE2ETests.swift b/Datadog/E2ETests/NTP/KronosE2ETests.swift new file mode 100644 index 0000000000..63d69d1cb9 --- /dev/null +++ b/Datadog/E2ETests/NTP/KronosE2ETests.swift @@ -0,0 +1,113 @@ +/* + * 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 DatadogLogs + +@testable import DatadogCore + +class KronosE2ETests: E2ETests { + /// The logger sending logs on Kronos execution. These logs are available in Mobile Integrations org. + private var logger: LoggerProtocol! // swiftlint:disable:this implicitly_unwrapped_optional + /// The logger sending telemetry on internal Kronos execution. These logs are available in Mobile Integrations org. + private let queue = DispatchQueue(label: "kronos-monitor-queue") + + override func setUp() { + super.setUp() + logger = Logger.create(with: Logger.Configuration(name: "kronos-e2e")) + } + + override func tearDown() { + logger = nil + super.tearDown() + } + + /// TODO: RUMM-1859: Add E2E tests for monitoring Kronos in nightly tests + func test_kronos_clock_performs_sync_using_datadog_ntp_pool() { // E2E:wip + /// The result of `KronosClock.sync()`. + struct KronosSyncResult { + /// First received server date. + var firstReceivedDate: Date? = nil + /// First received server offset. + var firstReceivedOffset: TimeInterval? = nil + /// Last received server date. + var lastReceivedDate: Date? = nil + /// Last received server offset. + var lastReceivedOffset: TimeInterval? = nil + /// Device date measured at the moment of receiving any server date. Used for additional debugging and comparision. + var measuredDeviceDate = Date() + } + + func performKronosSync(using pool: String) -> KronosSyncResult { + let kronosClock = KronosClock() + defer { kronosClock.reset() } + + // Given + let numberOfSamplesForEachIP = 2 // exchange only 2 samples with each resolved IP - to run test quick + + // Each IP (each server) is asked in parallel, but samples are obtained sequentially. + // Here we compute test timeout, to ensure that all (parallel) servers complete querying their (sequential) samples + // below `testTimeout` with assuming +50% margin. This should guarantee no flakiness on test timeout. + let testTimeout = kronosDefaultTimeout * Double(numberOfSamplesForEachIP) * 1.5 + + // When + let completionExpectation = expectation(description: "KronosClock.sync() calls completion closure") + var result = KronosSyncResult() + + kronosClock.sync( + from: pool, + samples: numberOfSamplesForEachIP, + first: { date, offset in // this closure could not be called if all samples to all servers resulted with failure + result.firstReceivedDate = date + result.firstReceivedOffset = offset + result.measuredDeviceDate = Date() + }, + completion: { date, offset in // this closure should always be called + result.lastReceivedDate = date + result.lastReceivedOffset = offset + result.measuredDeviceDate = Date() + completionExpectation.fulfill() + } + ) + + // Then + + // We don't expect receiving timeout on `completionExpectation`. Number of samples and individual sample timeout + // is configured in a way that lets `KronosNTPClient` always fulfill the `completionExpectation`. + waitForExpectations(timeout: testTimeout) + + return result + } + + // Run test for each Datadog NTP pool: + DatadogNTPServers.forEach { ddNTPPool in + let result = measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + performKronosSync(using: ddNTPPool) + } + + // Report result for this pool: + if let _ = result.firstReceivedDate, let _ = result.firstReceivedOffset, let serverDate = result.lastReceivedDate, let serverOffset = result.lastReceivedOffset { + // We consider `KronosClock.sync()` result to be consistent only if it has both `first` and `last` time values set. + // We log consistent result as INFO log that can be seen in Mobile Integration org. + logger.info("KronosClock.sync() completed with consistent result for \(ddNTPPool)", attributes: [ + "serverOffset_measured": serverDate.timeIntervalSince(result.measuredDeviceDate), + "serverOffset_received": serverOffset, + "serverDate_received": iso8601DateFormatter.string(from: serverDate), + ]) + } else { + // Inconsistent result may correspond to flaky execution, e.g. if network was unreachable or if **all** NTP calls received timeout. + // We track inconsistent result as WARN log that will be watched by E2E monitor. + logger.warn("KronosClock.sync() completed with inconsistent result for \(ddNTPPool)", attributes: [ + "serverDate_firstReceived": result.firstReceivedDate.flatMap { iso8601DateFormatter.string(from: $0) }, + "serverDate_lastReceived": result.lastReceivedDate.flatMap { iso8601DateFormatter.string(from: $0) }, + "serverOffset_firstReceived": result.firstReceivedOffset, + "serverOffset_lastReceived": result.lastReceivedOffset, + ]) + } + } + } +} diff --git a/Datadog/E2ETests/RUM/RUMGlobalE2ETests.swift b/Datadog/E2ETests/RUM/RUMGlobalE2ETests.swift new file mode 100644 index 0000000000..88ed8f3f2f --- /dev/null +++ b/Datadog/E2ETests/RUM/RUMGlobalE2ETests.swift @@ -0,0 +1,310 @@ +/* + * 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 TestUtilities +import DatadogRUM + +class RUMGlobalE2ETests: E2ETests { + private var rum: RUMMonitorProtocol { RUMMonitor.shared() } + + // MARK: - Common Monitors + + /// ```apm + /// $feature = rum + /// $monitor_id = rum_globalrum_add_attribute + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_globalrum_add_attribute: has a high average execution time" + /// $monitor_query = "sum(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_globalrum_add_attribute,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + /// + /// ```apm + /// $feature = rum + /// $monitor_id = rum_globalrum_remove_attribute + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_globalrum_remove_attribute: has a high average execution time" + /// $monitor_query = "sum(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_globalrum_remove_attribute,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + + // MARK: - RUM manual APIs + + /// - api-surface: RUMMonitorProtocol.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_add_attribute_for_view + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_add_attribute_for_view: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_add_attribute_for_view @type:view @view.name:rumView* @view.url_path:datadog\\/rum* @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_add_attribute_for_view() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + } + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + } + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.removeAttribute(forKey key: AttributeKey) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_remove_attribute_for_view + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_remove_attribute_for_view: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_remove_attribute_for_view @type:view @view.name:rumView* @view.url_path:datadog\\/rum* @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + func test_rum_globalrum_remove_attribute_for_view() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + } + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_add_attribute_for_action + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_add_attribute_for_action: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_add_attribute_for_action @type:action @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_add_attribute_for_action() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + } + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + } + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.addAction(type: .custom, name: actionName, attributes: DD.logAttributes()) + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + /// - api-surface: RUMMonitorProtocol.removeAttribute(forKey key: AttributeKey) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_remove_attribute_for_action + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_remove_attribute_for_action: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_remove_attribute_for_action @type:action @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_remove_attribute_for_action() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + } + + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + rum.addAction(type: .custom, name: actionName, attributes: DD.logAttributes()) + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_add_attribute_for_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_add_attribute_for_resource: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_add_attribute_for_resource @type:resource @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_add_attribute_for_resource() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + } + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + } + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + Thread.sleep(forTimeInterval: RUMConstants.writeDelay) + + rum.stopView(key: viewKey, attributes: [:]) + + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + /// - api-surface: RUMMonitorProtocol.removeAttribute(forKey key: AttributeKey) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_remove_attribute_for_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_remove_attribute_for_resource: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_remove_attribute_for_resource @type:resource @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_remove_attribute_for_resource() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + } + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + Thread.sleep(forTimeInterval: RUMConstants.writeDelay) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAttribute(forKey key: AttributeKey, value: AttributeValue) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_add_attribute_for_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_add_attribute_for_error: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_add_attribute_for_error @type:resource @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_add_attribute_for_error() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let errorMessage = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + } + measure(resourceName: DD.PerfSpanName.rumAttributeAddAttribute) { + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + } + + rum.addError( + message: errorMessage, + stack: nil, + source: .source, + attributes: DD.logAttributes(), + file: nil, + line: nil + ) + Thread.sleep(forTimeInterval: RUMConstants.writeDelay) + + rum.stopView(key: viewKey, attributes: [:]) + + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + /// - api-surface: RUMMonitorProtocol.removeAttribute(forKey key: AttributeKey) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_globalrum_remove_attribute_for_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_globalrum_remove_attribute_for_error: number of actions is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_globalrum_remove_attribute_for_error @type:resource @context.custom_attribute.int:* @context.custom_attribute.string:*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + func test_rum_globalrum_remove_attribute_for_error() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let errorMessage = String.mockRandom() + let strAttrValue = String.mockRandom() + let intAttrValue = Int.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.addAttribute(forKey: RUMConstants.customAttribute_String, value: strAttrValue) + rum.addAttribute(forKey: RUMConstants.customAttribute_Int, value: intAttrValue) + + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_String) + } + measure(resourceName: DD.PerfSpanName.rumAttributeRemoveAttribute) { + rum.removeAttribute(forKey: RUMConstants.customAttribute_Int) + } + + rum.addError( + message: errorMessage, + stack: nil, + source: .source, + attributes: DD.logAttributes(), + file: nil, + line: nil + ) + Thread.sleep(forTimeInterval: RUMConstants.writeDelay) + + rum.stopView(key: viewKey, attributes: [:]) + } +} diff --git a/Datadog/E2ETests/RUM/RUMMonitorE2ETests.swift b/Datadog/E2ETests/RUM/RUMMonitorE2ETests.swift new file mode 100644 index 0000000000..b34897b0db --- /dev/null +++ b/Datadog/E2ETests/RUM/RUMMonitorE2ETests.swift @@ -0,0 +1,1034 @@ +/* + * 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 TestUtilities +import DatadogRUM + +class RUMMonitorE2ETests: E2ETests { + private var rum: RUMMonitorProtocol { RUMMonitor.shared() } + + let actionTypePool = [RUMActionType.swipe, .scroll, .tap, .custom] + let nonCustomActionTypePool = [RUMActionType.swipe, .scroll, .tap] + + /// - api-surface: RUMMonitorProtocol.startView(key: String,name: String? = nil,attributes: [AttributeKey: AttributeValue] = [:]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_start_view + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_start_view: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_start_view @type:view @view.name:rumView* @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_start_view_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_start_view: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_start_view,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_start_view() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.stopView(key: String,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_view + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_view: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_view @type:view @view.name:rumView* @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_view_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_view: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_view,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_view() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopView(key: viewKey, attributes: [:]) + } + } + + /// - api-surface: RUMMonitorProtocol.stopView(key: String,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_view_with_pending_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_view_with_pending_resource: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_view_with_pending_resource @type:view @view.name:rumView* @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_view_with_pending_resource_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_view_with_pending_resource: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_view_with_pending_resource,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_view_with_pending_resource() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + rum.startResource(resourceKey: resourceKey, httpMethod: .get, urlString: resourceKey, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopView(key: viewKey, attributes: [:]) + } + + rum.stopResource(resourceKey: resourceKey, statusCode: (200...500).randomElement()!, kind: .other) + } + + /// - api-surface: RUMMonitorProtocol.stopView(key: String,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_view_with_pending_action + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_view_with_pending_action: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_view_with_pending_action @type:view @view.name:rumView* @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_view_with_pending_action_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_view_with_pending_action: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_view_with_pending_action,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_view_with_pending_action() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopView(key: viewKey, attributes: [:]) + } + + rum.stopAction(type: actionType, name: actionName, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addTiming(name: String) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_timing + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_timing: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_add_timing @type:view @view.name:rumView* @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_timing_upper_bound + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_timing: timing is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_add_timing @type:view @view.url_path:datadog\\/rum*\").rollup(\"avg\", \"@view.custom_timings.time_event\").last(\"1d\") < 200000000" + /// $monitor_threshold = 200000000 + /// ``` + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_timing_lower_bound + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_timing: timing is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_add_timing_lower_bound @type:view @view.url_path:datadog\\/rum*\").rollup(\"avg\", \"@view.custom_timings.time_event\").last(\"1d\") > 700000000" + /// $monitor_threshold = 700000000 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_timing_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_timing: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_timing,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_timing() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let timing = Double((200...700).randomElement()!) * 0.01 + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + Thread.sleep(forTimeInterval: timing) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addTiming(name: RUMConstants.timingName) + } + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_start_non_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_start_non_custom_action_with_no_outcome: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_start_non_custom_action_with_no_outcome\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_start_non_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_start_non_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_start_non_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_start_non_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = nonCustomActionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_start_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_start_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_start_custom_action_with_no_outcome @action.type:custom @view.url_path:datadog\\/rum* @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_start_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_start_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_start_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_start_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = RUMActionType.custom + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_start_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_start_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_start_action_with_outcome @view.url_path:datadog\\/rum* @action.name:*rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_start_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_start_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_start_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_start_action_with_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + rum.sendRandomActionOutcomeEvent() + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.stopAction(type: RUMActionType, name: String?, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_non_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_non_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_stop_non_custom_action_with_no_outcome\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_non_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_non_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_non_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_non_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = nonCustomActionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopAction(type: actionType, name: actionName, attributes: [:]) + } + + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.stopAction(type: RUMActionType, name: String?, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_stop_custom_action_with_no_outcome @action.type:custom @view.url_path:datadog\\/rum* @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_custom_action_with_no_outcome_performance_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = RUMActionType.custom + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopAction(type: actionType, name: actionName, attributes: [:]) + } + + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.stopAction(type: RUMActionType, name: String?, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_stop_action_with_outcome @view.url_path:datadog\\/rum* @action.name:*rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_action_with_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopAction(type: actionType, name: actionName, attributes: [:]) + } + + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_non_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_non_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_add_non_custom_action_with_no_outcome\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_non_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_non_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_non_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_non_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = nonCustomActionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_add_custom_action_with_no_outcome @action.type:custom @view.url_path:datadog\\/rum* @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_custom_action_with_no_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = RUMActionType.custom + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_add_action_with_outcome @view.url_path:datadog\\/rum* @action.name:*rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_action_with_outcome() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let actionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + rum.sendRandomActionOutcomeEvent() + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.stopAction(type: RUMActionType, name: String?, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_custom_action_while_active_action + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_custom_action_while_active_action: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_add_custom_action_while_active_action @action.type:custom @view.url_path:datadog\\/rum* @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_custom_action_while_active_action_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_custom_action_while_active_action: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_custom_action_while_active_action,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_custom_action_while_active_action() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let activeActionName = "rumActiveAction" + String.mockRandom() + let customActionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startAction(type: actionType, name: activeActionName, attributes: DD.logAttributes()) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: .custom, name: customActionName, attributes: DD.logAttributes()) + } + rum.sendRandomActionOutcomeEvent() + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + rum.stopAction(type: actionType, name: activeActionName, attributes: [:]) + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// - api-surface: RUMMonitorProtocol.stopAction(type: RUMActionType, name: String?, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_stop_background_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_ignore_stop_background_action_with_outcome @action.name:*rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_stop_background_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_stop_background_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_stop_background_action_with_outcome() { + let actionName = String.mockRandom() + let actionType = actionTypePool.randomElement()! + + rum.startAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + rum.sendRandomActionOutcomeEvent() + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopAction(type: actionType, name: actionName, attributes: [:]) + } + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome @action.type:custom @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_non_custom_action_with_no_outcome() { + let actionName = String.mockRandom() + let actionType = nonCustomActionTypePool.randomElement()! + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_custom_action_with_no_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_custom_action_with_no_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_ignore_add_background_custom_action_with_no_outcome @action.type:custom @action.name:rumAction* @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").by(\"@type\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_custom_action_with_no_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_custom_action_with_no_outcome: has a high average execution time" + /// $monitor_query = "sum(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_custom_action_with_no_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_custom_action_with_no_outcome() { + let actionName = String.mockRandom() + let actionType = RUMActionType.custom + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_custom_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_custom_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_ignore_add_background_custom_action_with_outcome @action.type:custom @action.name:rumAction* @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").by(\"@type\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_custom_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_custom_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_custom_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_custom_action_with_outcome() { + let actionName = String.mockRandom() + let actionType = RUMActionType.custom + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + rum.sendRandomActionOutcomeEvent() + } + + /// - api-surface: RUMMonitorProtocol.addAction(type: RUMActionType, name: String, attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_non_custom_action_with_outcome + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_non_custom_action_with_outcome: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_rummonitor_ignore_add_background_non_custom_action_with_outcome @action.type:custom @action.name:rumAction*\").rollup(\"count\").by(\"@type\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_non_custom_action_with_outcome_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_non_custom_action_with_outcome: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_non_custom_action_with_outcome,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_non_custom_action_with_outcome() { + let actionName = String.mockRandom() + let actionType = nonCustomActionTypePool.randomElement()! + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addAction(type: actionType, name: actionName, attributes: DD.logAttributes()) + } + Thread.sleep(forTimeInterval: RUMConstants.actionInactivityThreshold) + rum.sendRandomActionOutcomeEvent() + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_start_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_start_resource: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_start_resource @view.url_path:datadog\\/rum* @type:resource\").rollup(\"count\").last(\"1d\") > 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_start_resource_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_start_resource: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_start_resource,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_start_resource() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// - api-surface: RUMMonitorProtocol.stopResource(resourceKey: String,statusCode: Int?,kind: RUMResourceType,size: Int64? = nil,attributes: [AttributeKey: AttributeValue] = [:]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_resource: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_resource @view.url_path:datadog\\/rum* @type:resource @resource.status_code:200 @resource.type:(beacon OR fetch OR xhr OR document OR unknown OR image OR js OR font OR css OR media OR other) @resource.url:http\\:\\/\\/datadog\\/resource\\/rum\\/*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_resource_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_resource: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_resource,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_resource() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopResource(resourceKey: resourceKey, statusCode: 200, kind: .other) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// - api-surface: RUMMonitorProtocol.stopResourceWithError(resourceKey: String,message: String,response: URLResponse?,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_resource_with_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_resource_with_error: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_resource_with_error @type:error @error.resource.status_code:>=400 @error.source:(logger OR network OR source OR console OR agent OR webview) @error.resource.url:http\\:\\/\\/datadog\\/resource\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_resource_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_resource_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_resource_with_error,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_resource_with_error() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopResourceWithError( + resourceKey: resourceKey, + message: String.mockRandom(), + response: HTTPURLResponse( + url: URL.mockRandom(), + statusCode: (400...511).randomElement()!, + httpVersion: nil, + headerFields: nil + ), + attributes: DD.logAttributes() + ) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// - api-surface: RUMMonitorProtocol.stopResourceWithError(resourceKey: String,message: String,response: URLResponse?,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_stop_resource_with_error_without_status_code + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_stop_resource_with_error_without_status_code: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_stop_resource_with_error_without_status_code @type:error @error.source:(logger OR network OR source OR console OR agent OR webview) @error.resource.url:http\\:\\/\\/datadog\\/resource\\/rum* @error.resource.status_code:0\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_stop_resource_with_error_without_status_code_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_stop_resource_with_error_without_status_code: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_stop_resource_with_error_without_status_code,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_stop_resource_with_error_without_status_code() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let resourceKey = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopResourceWithError( + resourceKey: resourceKey, + message: String.mockRandom(), + response: nil, + attributes: DD.logAttributes() + ) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// - api-surface: RUMMonitorProtocol.stopResource(resourceKey: String,statusCode: Int?,kind: RUMResourceType,size: Int64? = nil,attributes: [AttributeKey: AttributeValue] = [:]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_resource + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_stop_background_resource: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_ignore_stop_background_resource @type:resource @resource.status_code:200 @resource.type:(beacon OR fetch OR xhr OR document OR unknown OR image OR js OR font OR css OR media OR other) @resource.url:http\\:\\/\\/datadog\\/resource\\/rum\\/* @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_resource_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_stop_background_resource: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_stop_background_resource,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_stop_background_resource() { + let resourceKey = String.mockRandom() + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopResource(resourceKey: resourceKey, statusCode: 200, kind: .other) + } + } + + /// - api-surface: RUMMonitorProtocol.startResource(resourceKey: String,httpMethod: RUMMethod,urlString: String,attributes: [AttributeKey: AttributeValue] = [:]) + /// - api-surface: RUMMonitorProtocol.stopResourceWithError(resourceKey: String,message: String,response: URLResponse?,attributes: [AttributeKey: AttributeValue]) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_resource_with_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_stop_background_resource_with_error: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_ignore_stop_background_resource_with_error @type:error @error.resource.url:http\\:\\/\\/datadog\\/resource\\/rum\\/* @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_stop_background_resource_with_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_stop_background_resource_with_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_stop_background_resource_with_error,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_stop_background_resource_with_error() { + let resourceKey = String.mockRandom() + + rum.startResource( + resourceKey: resourceKey, + httpMethod: .get, + urlString: String.mockRandom(), + attributes: DD.logAttributes() + ) + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.stopResourceWithError( + resourceKey: resourceKey, + message: String.mockRandom(), + response: HTTPURLResponse( + url: URL.mockRandom(), + statusCode: (400...511).randomElement()!, + httpVersion: nil, + headerFields: nil + ), + attributes: DD.logAttributes() + ) + } + } + + /// - api-surface: RUMMonitorProtocol.addError(message: String,source: RUMErrorSource,stack: String?,attributes: [AttributeKey: AttributeValue],file: StaticString?,line: UInt?) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_error: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_add_error @type:error @error.source:(logger OR network OR source OR console OR agent OR webview) @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_error,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_error() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let errorMessage = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addError(message: errorMessage, stack: nil, source: .custom, attributes: DD.logAttributes(), file: nil, line: nil) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addError(message: String,source: RUMErrorSource,stack: String?,attributes: [AttributeKey: AttributeValue],file: StaticString?,line: UInt?) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_add_error_with_stacktrace + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_add_error_with_stacktrace: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_add_error_with_stacktrace @type:error @error.source:(logger OR network OR source OR console OR agent OR webview) @view.url_path:datadog\\/rum*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_add_error_with_stacktrace_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_add_error_with_stacktrace: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_add_error_with_stacktrace,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_add_error_with_stacktrace() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + let errorMessage = String.mockRandom() + + rum.startView(key: viewKey, name: viewName, attributes: DD.logAttributes()) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addError( + message: errorMessage, + stack: String.mockRandom(), + source: .custom, + attributes: DD.logAttributes(), + file: nil, + line: nil + ) + } + + rum.stopView(key: viewKey, attributes: [:]) + } + + /// - api-surface: RUMMonitorProtocol.addError(message: String,source: RUMErrorSource,stack: String?,attributes: [AttributeKey: AttributeValue],file: StaticString?,line: UInt?) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_error + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_error: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_ignore_add_background_error @type:error @error.source:(logger OR network OR source OR console OR agent OR webview) @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_error_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_error: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_error,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_error() { + let errorMessage = String.mockRandom() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addError(message: errorMessage, stack: nil, source: .custom, attributes: DD.logAttributes(), file: nil, line: nil) + } + } + + /// - api-surface: RUMMonitorProtocol.addError(message: String,source: RUMErrorSource,stack: String?,attributes: [AttributeKey: AttributeValue],file: StaticString?,line: UInt?) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_rummonitor_ignore_add_background_error_with_stacktrace + /// $monitor_name = "[RUM] [iOS] Nightly - rum_rummonitor_ignore_add_background_error_with_stacktrace: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @context.test_method_name:rum_rummonitor_ignore_add_background_error_with_stacktrace @type:error @error.source:(logger OR network OR source OR console OR agent OR webview) @view.url_path:\"com/datadog/background/view\"\").rollup(\"count\").last(\"1d\") > 0" + /// $monitor_threshold = 0.0 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = rum + /// $monitor_id = rum_rummonitor_ignore_add_background_error_with_stacktrace_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - rum_rummonitor_ignore_add_background_error_with_stacktrace: has a high average execution time" + /// $monitor_query = "avg(last_1d):avg:trace.perf_measure{env:instrumentation,resource_name:rum_rummonitor_ignore_add_background_error_with_stacktrace,service:com.datadog.ios.nightly} > 0.024" + /// $monitor_threshold = 0.024 + /// ``` + func test_rum_rummonitor_ignore_add_background_error_with_stacktrace() { + let errorMessage = String.mockRandom() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + rum.addError(message: errorMessage, stack: String.mockRandom(), source: .custom, attributes: DD.logAttributes(), file: nil, line: nil) + } + } +} diff --git a/Datadog/E2ETests/RUM/RUMTrackingConsentE2ETests.swift b/Datadog/E2ETests/RUM/RUMTrackingConsentE2ETests.swift new file mode 100644 index 0000000000..e2820b5573 --- /dev/null +++ b/Datadog/E2ETests/RUM/RUMTrackingConsentE2ETests.swift @@ -0,0 +1,222 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogCore +import DatadogRUM + +class RUMTrackingConsentE2ETests: E2ETests { + private var rum: RUMMonitorProtocol { RUMMonitor.shared() } + + override func setUp() { + skipSDKInitialization = true // we will initialize it in each test + super.setUp() + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_pending + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_pending: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_pending\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_pending() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + RUM.enable(with: .e2e) + } + rum.sendRandomRUMEvent() + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_granted: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + func test_rum_config_consent_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + RUM.enable(with: .e2e) + } + rum.sendRandomRUMEvent() + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_not_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_not_granted: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_not_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_not_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + RUM.enable(with: .e2e) + } + rum.sendRandomRUMEvent() + } + + /// - api-surface: RUMMonitorProtocol.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_pending_to_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_pending_to_granted: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_pending_to_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + func test_rum_config_consent_pending_to_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .granted) + } + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_pending_to_not_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_pending_to_not_granted: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_pending_to_not_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_pending_to_not_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .pending + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .notGranted) + } + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_granted_to_not_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_granted_to_not_granted: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_granted_to_not_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_granted_to_not_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .notGranted) + } + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_granted_to_pending + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_granted_to_pending: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_granted_to_pending\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_granted_to_pending() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .pending) + } + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_not_granted_to_granted + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_not_granted_to_granted: number of views is below expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_not_granted_to_granted\").rollup(\"count\").by(\"@type\").last(\"1d\") < 1" + /// ``` + func test_rum_config_consent_not_granted_to_granted() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .granted) + } + } + + /// - api-surface: RUM.enable() -> RUMMonitorProtocol + /// - api-surface: Datadog.set(trackingConsent: TrackingConsent) + /// + /// - data monitor: + /// ```rum + /// $monitor_id = rum_config_consent_not_granted_to_pending + /// $monitor_name = "[RUM] [iOS] Nightly - rum_config_consent_not_granted_to_pending: number of views is above expected value" + /// $monitor_query = "rum(\"service:com.datadog.ios.nightly @type:action @context.test_method_name:rum_config_consent_not_granted_to_pending\").rollup(\"count\").by(\"@type\").last(\"1d\") > 1" + /// ``` + func test_rum_config_consent_not_granted_to_pending() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .notGranted + ) + + RUM.enable(with: .e2e) + } + rum.dd.sendRandomRUMEvent() + measure(resourceName: DD.PerfSpanName.setTrackingConsent) { + Datadog.set(trackingConsent: .pending) + } + } +} diff --git a/Datadog/E2ETests/RUM/RUMUtils.swift b/Datadog/E2ETests/RUM/RUMUtils.swift new file mode 100644 index 0000000000..d32c4e953b --- /dev/null +++ b/Datadog/E2ETests/RUM/RUMUtils.swift @@ -0,0 +1,66 @@ +/* + * 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 DatadogRUM +import TestUtilities + +internal struct RUMConstants { + static let customAttribute_String = "custom_attribute.string" + static let customAttribute_Int = "custom_attribute.int" + + static let actionInactivityThreshold = 0.1 + static let writeDelay = 0.1 + + static let timingName = "custom timing" +} + +internal extension RUMMonitorProtocol { + func sendRandomRUMEvent() { + let viewKey = String.mockRandom() + let viewName = String.mockRandom() + + // swiftlint:disable opening_brace + let RUMEvents: [() -> Void] = [ + { + self.startView(key: viewKey, name: viewName, attributes: [:]) + self.stopView(key: viewKey, attributes: [:]) + }, + { + let resourceKey = String.mockRandom() + self.startView(key: viewKey, name: viewName, attributes: [:]) + self.startResource(resourceKey: resourceKey, httpMethod: .get, urlString: String.mockRandom(), attributes: [:]) + self.stopResource(resourceKey: resourceKey, statusCode: (200...500).randomElement()!, kind: .other) + self.stopView(key: viewKey, attributes: [:]) + }, + { + self.startView(key: viewKey, name: viewName, attributes: [:]) + self.addError(message: String.mockRandom(), stack: String.mockRandom(), source: .custom, attributes: [:], file: nil, line: nil) + self.stopView(key: viewKey, attributes: [:]) + }, + { + let actionName = String.mockRandom() + self.startView(key: viewKey, name: viewName, attributes: [:]) + self.addAction(type: [RUMActionType.swipe, .scroll, .tap, .custom].randomElement()!, name: actionName, attributes: [:]) + self.sendRandomActionOutcomeEvent() + self.stopView(key: viewKey, attributes: [:]) + } + ] + // swiftlint:enable opening_brace + let randomEvent = RUMEvents.randomElement()! + randomEvent() + } + + func sendRandomActionOutcomeEvent() { + if Bool.random() { + let key = String.mockRandom() + self.startResource(resourceKey: key, httpMethod: .get, urlString: key) + self.stopResource(resourceKey: key, statusCode: (200...500).randomElement()!, kind: .other) + } else { + self.addError(message: String.mockRandom(), stack: nil, source: .custom, attributes: [:], file: nil, line: nil) + } + } +} diff --git a/Datadog/E2ETests/Tracing/SpanE2ETests.swift b/Datadog/E2ETests/Tracing/SpanE2ETests.swift new file mode 100644 index 0000000000..4d9d835d29 --- /dev/null +++ b/Datadog/E2ETests/Tracing/SpanE2ETests.swift @@ -0,0 +1,181 @@ +/* + * 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 DatadogTrace + +class SpanE2ETests: E2ETests { + /// - api-surface: OTSpan.setOperationName(_ operationName: String) + /// + /// - data monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_operation_name_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_set_operation_name: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:trace.trace_span_set_operation_name.hits{service:com.datadog.ios.nightly,env:instrumentation}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_operation_name_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_set_operation_name: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_set_operation_name,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_set_operation_name() { + let span = Tracer.shared().startSpan(operationName: .mockRandom()) + let knownOperationName = "trace_span_set_operation_name_new_operation_name" // asserted in monitor + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.setOperationName(knownOperationName) + } + + span.finish() + } + + /// - api-surface: OTSpan.setTag(key: String, value: Encodable) + /// + /// - data monitor: (it uses `ios_trace_span_set_tag` metric defined in "APM > Generate Metrics > Custom Span Metrics") + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_tag_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_set_tag: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:ios_trace_span_set_tag.hits_with_proper_payload{*}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_tag_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_set_tag: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_set_tag,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_set_tag() { + let span = Tracer.shared().startSpan(operationName: "ios_trace_span_set_tag") + let knownTag = DD.specialTag() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.setTag(key: knownTag.key, value: knownTag.value) + } + + span.finish() + } + + /// - api-surface: OTSpan.setBaggageItem(key: String, value: String) + /// + /// - data monitor: (it uses `ios_trace_span_set_baggage_item` metric defined in "APM > Generate Metrics > Custom Span Metrics") + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_baggage_item_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_set_baggage_item: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:ios_trace_span_set_baggage_item.hits_with_proper_payload{*}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_baggage_item_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_set_baggage_item: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_set_baggage_item,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_set_baggage_item() { + let span = Tracer.shared().startSpan(operationName: "ios_trace_span_set_baggage_item") + let knownTag = DD.specialTag() + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.setBaggageItem(key: knownTag.key, value: knownTag.value) + } + + span.finish() + } + + /// - api-surface: OTTracer.activeSpan: OTSpan? + /// + /// - data monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_active_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_set_active: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:trace.trace_span_set_active_measured_span.hits{service:com.datadog.ios.nightly,env:instrumentation}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_set_active_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_set_active: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_set_active,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_set_active() { + let span = Tracer.shared().startSpan(operationName: "trace_span_set_active_measured_span") + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.setActive() + } + + span.finish() + } + + /// - api-surface: OTTracer.log(fields: [String: Encodable], timestamp: Date) + /// + /// - data monitor: + /// ```logs + /// $feature = trace + /// $monitor_id = trace_span_log_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_log: number of hits is below expected value" + /// $monitor_query = "logs(\"service:com.datadog.ios.nightly @test_method_name:trace_span_log @test_special_string_attribute:customAttribute*\").index(\"*\").rollup(\"count\").last(\"1d\") < 1" + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_log_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_log: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_log,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_log() { + let span = Tracer.shared().startSpan(operationName: "trace_span_log_measured_span") + let log = DD.specialStringAttribute() + + var fields = DD.logAttributes() + fields[log.key] = log.value + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.log(fields: fields) + } + + span.finish() + } + + /// - api-surface: OTTracer.finish(at time: Date) + /// + /// - data monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_finish_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_span_finish: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:trace.trace_span_finish_measured_span.hits{service:com.datadog.ios.nightly,env:instrumentation}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_span_finish_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_span_finish: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_span_finish,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_span_finish() { + let span = Tracer.shared().startSpan(operationName: "trace_span_finish_measured_span") + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + span.finish() + } + } +} diff --git a/Datadog/E2ETests/Tracing/TraceConfigurationE2ETests.swift b/Datadog/E2ETests/Tracing/TraceConfigurationE2ETests.swift new file mode 100644 index 0000000000..8154f59667 --- /dev/null +++ b/Datadog/E2ETests/Tracing/TraceConfigurationE2ETests.swift @@ -0,0 +1,71 @@ +/* + * 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 DatadogCore +import DatadogTrace +import DatadogLogs +import DatadogRUM +import DatadogCrashReporting + +class TraceConfigurationE2ETests: E2ETests { + override func setUp() { + skipSDKInitialization = true // we will initialize it in each test + super.setUp() + } + + /// - api-surface: Trace.enable() + /// + /// - data monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_config_feature_enabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_config_feature_enabled: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:trace.trace_config_feature_enabled_observed_span.hits{service:com.datadog.ios.nightly,env:instrumentation}.as_count() < 1" + /// $monitor_threshold = 1 + /// ``` + func test_trace_config_feature_enabled() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + Trace.enable() + RUM.enable(with: .e2e) + CrashReporting.enable() + } + + let span = Tracer.shared().startRootSpan(operationName: "trace_config_feature_enabled_observed_span") + span.finish() + } + + /// - api-surface: Trace.enable() + /// + /// - data monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_config_feature_disabled_data + /// $monitor_name = "[RUM] [iOS] Nightly - trace_config_feature_disabled: number of hits is below expected value" + /// $monitor_query = "sum(last_1d):avg:trace.trace_config_feature_disabled_observed_span.hits{service:com.datadog.ios.nightly,env:instrumentation}.as_count() > 0" + /// $monitor_threshold = 0 + /// ``` + func test_trace_config_feature_disabled() { + measure(resourceName: DD.PerfSpanName.sdkInitialize) { + Datadog.initialize( + with: .e2e, + trackingConsent: .granted + ) + + Logs.enable() + RUM.enable(with: .e2e) + CrashReporting.enable() + } + + let span = Tracer.shared().startRootSpan(operationName: "test_trace_config_feature_disabled_observed_span") + span.finish() + } +} diff --git a/Datadog/E2ETests/Tracing/TracerE2ETests.swift b/Datadog/E2ETests/Tracing/TracerE2ETests.swift new file mode 100644 index 0000000000..f56bc03e76 --- /dev/null +++ b/Datadog/E2ETests/Tracing/TracerE2ETests.swift @@ -0,0 +1,75 @@ +/* + * 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 DatadogCore +import DatadogTrace + +class TracerE2ETests: E2ETests { + private var tracer: OTTracer { Tracer.shared() } + + /// - api-surface: OTTracer.startSpan(operationName: String,references: [OTReference]?,tags: [String: Encodable]?,startTime: Date?) -> OTSpan + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_tracer_start_span_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_tracer_start_span: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_tracer_start_span,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_tracer_start_span() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + _ = tracer.startSpan(operationName: .mockRandom()) // this span is never sent + } + } + + /// - api-surface: OTTracer.startRootSpan(operationName: String,tags: [String: Encodable]?,startTime: Date?) -> OTSpan + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_tracer_start_root_span_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_tracer_start_root_span: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_tracer_start_root_span,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_tracer_start_root_span() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + _ = tracer.startRootSpan(operationName: .mockRandom()) // this span is never sent + } + } + + /// - api-surface: OTTracer.inject(spanContext: OTSpanContext, writer: OTFormatWriter) + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_tracer_inject_span_context_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_tracer_inject_span_context: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_tracer_inject_span_context,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_tracer_inject_span_context() { + let anySpan = tracer.startSpan(operationName: .mockRandom()) // this span is never sent + let anyWriter = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 20), traceContextInjection: .all) + + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + tracer.inject(spanContext: anySpan.context, writer: anyWriter) + } + } + + /// - api-surface: OTTracer.activeSpan: OTSpan? + /// + /// - performance monitor: + /// ```apm + /// $feature = trace + /// $monitor_id = trace_tracer_active_span_performance + /// $monitor_name = "[RUM] [iOS] Nightly Performance - trace_tracer_active_span: has a high average execution time" + /// $monitor_query = "avg(last_1d):p50:trace.perf_measure{env:instrumentation,resource_name:trace_tracer_active_span,service:com.datadog.ios.nightly} > 0.024" + /// ``` + func test_trace_tracer_active_span() { + measure(resourceName: DD.PerfSpanName.fromCurrentMethodName()) { + _ = tracer.activeSpan + } + } +} diff --git a/Datadog/Example/AppConfig.swift b/Datadog/Example/AppConfig.swift deleted file mode 100644 index 3a753f4e1c..0000000000 --- a/Datadog/Example/AppConfig.swift +++ /dev/null @@ -1,89 +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-2020 Datadog, Inc. - */ - -import Foundation -import Datadog - -protocol AppConfig { - /// Service name used for logs and traces. - var serviceName: String { get } - /// SDK configuration - var datadogConfiguration: Datadog.Configuration { get } - /// Endpoints for arbitrary network requests - var arbitraryNetworkURL: URL { get } - var arbitraryNetworkRequest: URLRequest { get } -} - -struct ExampleAppConfig: AppConfig { - /// Service name used for logs and traces. - let serviceName = "ios-sdk-example-app" - /// Configuration for uploading logs to Datadog servers - let datadogConfiguration: Datadog.Configuration - - let arbitraryNetworkURL = URL(string: "https://status.datadoghq.com")! - let arbitraryNetworkRequest: URLRequest = { - var request = URLRequest(url: URL(string: "https://status.datadoghq.com/bad/path")!) - request.httpMethod = "POST" - request.addValue("dataTaskWithRequest", forHTTPHeaderField: "creation-method") - return request - }() - - init() { - guard let clientToken = Bundle.main.infoDictionary?["DatadogClientToken"] as? String, !clientToken.isEmpty else { - fatalError(""" - ✋⛔️ Cannot read `DATADOG_CLIENT_TOKEN` from `Info.plist` dictionary. - Please update `Datadog.xcconfig` in the repository root with your own - client token obtained on datadoghq.com. - You might need to run `Product > Clean Build Folder` before retrying. - """) - } - - self.datadogConfiguration = Datadog.Configuration - .builderUsing(clientToken: clientToken, environment: "tests") - .set(tracedHosts: [arbitraryNetworkURL.host!, "foo.bar"]) - .build() - } -} - -struct UITestAppConfig: AppConfig { - /// Mocked service name for UITests - let serviceName = "ui-tests-service-name" - /// Configuration for uploading logs to mock servers - let datadogConfiguration: Datadog.Configuration - let arbitraryNetworkURL: URL - let arbitraryNetworkRequest: URLRequest - - init() { - let mockLogsEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_LOGS_ENDPOINT_URL"]! - let mockTracesEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_TRACES_ENDPOINT_URL"]! - let sourceEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_SOURCE_ENDPOINT_URL"]! - let tracedhost = URL(string: sourceEndpoint)!.host! - self.datadogConfiguration = Datadog.Configuration - .builderUsing(clientToken: "ui-tests-client-token", environment: "integration") - .set(logsEndpoint: .custom(url: mockLogsEndpoint)) - .set(tracesEndpoint: .custom(url: mockTracesEndpoint)) - .set(tracedHosts: [tracedhost, "foo.bar"]) - .build() - - let url = URL(string: sourceEndpoint)! - self.arbitraryNetworkURL = URL(string: url.deletingLastPathComponent().absoluteString + "inspect")! - self.arbitraryNetworkRequest = { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("dataTaskWithRequest", forHTTPHeaderField: "creation-method") - return request - }() - } -} - -/// Returns different `AppConfig` when running in UI Tests or launching directly. -func currentAppConfig() -> AppConfig { - if ProcessInfo.processInfo.arguments.contains("IS_RUNNING_UI_TESTS") { - return UITestAppConfig() - } else { - return ExampleAppConfig() - } -} diff --git a/Datadog/Example/AppDelegate.swift b/Datadog/Example/AppDelegate.swift deleted file mode 100644 index 8bb0e6a73d..0000000000 --- a/Datadog/Example/AppDelegate.swift +++ /dev/null @@ -1,102 +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-2020 Datadog, Inc. -*/ - -import UIKit -import Datadog - -var logger: Logger! -var tracer: OTTracer { Global.sharedTracer } - -let appConfig: AppConfig = currentAppConfig() - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - if isRunningUnitTests() { - window = nil - return false - } - - if isRunningUITests() { - deletePersistedSDKData() - } - - // Initialize Datadog SDK - Datadog.initialize( - appContext: .init(), - configuration: appConfig.datadogConfiguration - ) - - // Set user information - Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com") - - // Create logger instance - logger = Logger.builder - .set(serviceName: appConfig.serviceName) - .set(loggerName: "logger-name") - .sendNetworkInfo(true) - .printLogsToConsole(true, usingFormat: .shortWith(prefix: "[iOS App] ")) - .build() - - // Register global tracer - Global.sharedTracer = Tracer.initialize( - configuration: Tracer.Configuration( - serviceName: appConfig.serviceName, - sendNetworkInfo: true - ) - ) - - // Set highest verbosity level to see internal actions made in SDK - Datadog.verbosityLevel = .debug - - // Add attributes - logger.addAttribute(forKey: "device-model", value: UIDevice.current.model) - - // Add tags - #if DEBUG - logger.addTag(withKey: "build_configuration", value: "debug") - #else - logger.addTag(withKey: "build_configuration", value: "release") - #endif - - return true - } - - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - installConsoleOutputInterceptor() - return true - } -} - -private func isRunningUnitTests() -> Bool { - return ProcessInfo.processInfo.arguments.contains("IS_RUNNING_UNIT_TESTS") -} - -private func isRunningUITests() -> Bool { - return appConfig is UITestAppConfig -} - -private func deletePersistedSDKData() { - guard let cachesDirectoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - return - } - - do { - let dataDirectories = try FileManager.default - .contentsOfDirectory(at: cachesDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey, .canonicalPathKey]) - .filter { $0.absoluteString.contains("com.datadoghq") } - - try dataDirectories.forEach { url in - try FileManager.default.removeItem(at: url) - print("🧹 Deleted SDK data directory: \(url)") - } - } catch { - print("🔥 Failed to delete SDK data directory: \(error)") - } -} diff --git a/Datadog/Example/Base.lproj/Main iOS.storyboard b/Datadog/Example/Base.lproj/Main iOS.storyboard new file mode 100644 index 0000000000..f2f1e7d959 --- /dev/null +++ b/Datadog/Example/Base.lproj/Main iOS.storyboard @@ -0,0 +1,2253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Base.lproj/Main.storyboard b/Datadog/Example/Base.lproj/Main.storyboard deleted file mode 100644 index 8b4f827307..0000000000 --- a/Datadog/Example/Base.lproj/Main.storyboard +++ /dev/null @@ -1,724 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Datadog/Example/Debugging/BackgroundEvents/BackgroundLocationMonitor.swift b/Datadog/Example/Debugging/BackgroundEvents/BackgroundLocationMonitor.swift new file mode 100644 index 0000000000..c5faacddd7 --- /dev/null +++ b/Datadog/Example/Debugging/BackgroundEvents/BackgroundLocationMonitor.swift @@ -0,0 +1,177 @@ +/* + * 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 CoreLocation +import UIKit.UIApplication +import DatadogRUM + +internal var backgroundLocationMonitor: BackgroundLocationMonitor? + +/// Location monitor used in "Example" app for debugging and testing iOS SDK features in background. +internal class BackgroundLocationMonitor: NSObject, CLLocationManagerDelegate { + private struct Constants { + static let locationMonitoringUserDefaultsKey = "is-location-monitoring-started" + static let crashOnNextBackgroundEventUserDefaultsKey = "crash-on-next-background-event" + static let crashDuringNextBackgroundLaunchUserDefaultsKey = "crash-during-next-background-launch" + } + + private let locationManager = CLLocationManager() + + /// Tells if location monitoring is started. + /// This setting is preserved between application launches. Defaults to `false`. + /// + /// Note: `BackgroundLocationMonitor` can be started independently from receiving location monitoring authorization status. + /// Even if this value is `true`, location updates might not be delivered due to restricted or denied status. + private(set) var isStarted: Bool { + get { UserDefaults.standard.bool(forKey: Constants.locationMonitoringUserDefaultsKey) } + set { UserDefaults.standard.set(newValue, forKey: Constants.locationMonitoringUserDefaultsKey) } + } + + /// If enabled, the Example app will crash on receiving next event in background. + /// This setting is preserved between application launches. Defaults to `false` and is reset to `false` shortly before crash. + private(set) var shouldCrashOnNextBackgroundEvent: Bool { + get { UserDefaults.standard.bool(forKey: Constants.crashOnNextBackgroundEventUserDefaultsKey) } + set { UserDefaults.standard.set(newValue, forKey: Constants.crashOnNextBackgroundEventUserDefaultsKey) } + } + + /// If enabled, the Example app will crash during next launch in background. + /// This setting is preserved between application launches. Defaults to `false` and is reset to `false` shortly before crash. + private(set) var shouldCrashDuringNextBackgroundLaunch: Bool { + get { UserDefaults.standard.bool(forKey: Constants.crashDuringNextBackgroundLaunchUserDefaultsKey) } + set { UserDefaults.standard.set(newValue, forKey: Constants.crashDuringNextBackgroundLaunchUserDefaultsKey) } + } + + private var isAppInBackground: Bool { + return UIApplication.shared.applicationState == .background + } + + private let rum: RUMMonitorProtocol + + /// Current authorization status for location monitoring. + var currentAuthorizationStatus: String { authorizationStatusDescription(for: locationManager) } + + /// Notifies change of authorization status for location monitoring. + var onAuthorizationStatusChange: ((String) -> Void)? = nil + + required init(rum: RUMMonitorProtocol) { + self.rum = rum + + super.init() + + if isStarted { + // If location monitoring was enabled in previous app session, here we start it for current session. + // This will keep location tracking when the app is woken up in background due to significant location change. + startMonitoring() + } + + if isAppInBackground && shouldCrashDuringNextBackgroundLaunch { + shouldCrashDuringNextBackgroundLaunch = false + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + fatalError("Crash during application launch in background") + } + } + } + + func startMonitoring() { + logger.debug("Starting 'BackgroundLocationMonitor' with authorizationStatus: '\(authorizationStatusDescription(for: locationManager))'") + + if CLLocationManager.significantLocationChangeMonitoringAvailable() { + locationManager.delegate = self + locationManager.allowsBackgroundLocationUpdates = true + + locationManager.requestAlwaysAuthorization() + locationManager.startMonitoringSignificantLocationChanges() + isStarted = true + } else { + rum.addError(message: "Significant location changes monitoring is not available") + } + } + + func stopMonitoring() { + locationManager.stopMonitoringSignificantLocationChanges() + isStarted = false + } + + func setCrashOnNextBackgroundEvent(_ enabled: Bool) { + shouldCrashOnNextBackgroundEvent = enabled + } + + func setCrashDuringNextBackgroundLaunch(_ enabled: Bool) { + shouldCrashDuringNextBackgroundLaunch = enabled + } + + // MARK: - CLLocationManagerDelegate + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + let status = authorizationStatusDescription(for: locationManager) + logger.debug("Changed 'BackgroundLocationMonitor' authorizationStatus: '\(status)'") + onAuthorizationStatusChange?(status) + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let recentLocation = locations.last else { + rum.addError(message: "Received update with no locations") + return + } + + logger.debug( + "Location changed at \(recentLocation.timestamp)", + attributes: [ + "latitude": recentLocation.coordinate.latitude, + "longitude": recentLocation.coordinate.longitude, + "speed": recentLocation.speed, + ] + ) + + rum.addAction( + type: .custom, + name: "Location changed at \(recentLocation.timestamp)", + attributes: [ + "latitude": recentLocation.coordinate.latitude, + "longitude": recentLocation.coordinate.longitude, + "speed": recentLocation.speed, + ] + ) + + if isAppInBackground && shouldCrashOnNextBackgroundEvent { + shouldCrashOnNextBackgroundEvent = false + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + fatalError("Crash on receiving event in background") + } + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + if let error = error as? CLError { + if error.code == .denied { + manager.stopMonitoringSignificantLocationChanges() + } + logger.error("Location manager failed with CLError (error.code: \(error.code)", error: error) + rum.addError(message: "Location manager failed with CLError (error.code: \(error.code)") + } else { + logger.error("Location manager failed", error: error) + rum.addError(error: error) + } + } + + // MARK: - Helpers + + private func authorizationStatusDescription(for manager: CLLocationManager) -> String { + if #available(iOS 14.0, *) { + switch locationManager.authorizationStatus { + case .authorizedAlways: return "authorizedAlways" + case .notDetermined: return "notDetermined" + case .restricted: return "restricted" + case .denied: return "denied" + case .authorizedWhenInUse: return "authorizedWhenInUse" + @unknown default: return "unrecognized (sth new)" + } + } else { + return "unavailable prior to iOS 14.0" + } + } +} diff --git a/Datadog/Example/Debugging/BackgroundEvents/DebugBackgroundEventsViewController.swift b/Datadog/Example/Debugging/BackgroundEvents/DebugBackgroundEventsViewController.swift new file mode 100644 index 0000000000..6a8bc62c91 --- /dev/null +++ b/Datadog/Example/Debugging/BackgroundEvents/DebugBackgroundEventsViewController.swift @@ -0,0 +1,131 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import SwiftUI +import DatadogCore + +@available(iOS 13, tvOS 13,*) +internal class DebugBackgroundEventsViewController: UIHostingController { + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: DebugBackgroundEventsView()) + } +} + +@available(iOS 13, tvOS 13,*) +private class DebugBackgroundEventsViewModel: ObservableObject { + private let locationMonitor: BackgroundLocationMonitor + + @Published var isLocationMonitoringON = false + @Published var willCrashDuringNextBackgroundLaunch = false + @Published var willCrashOnNextBackgroundEvent = false + @Published var authorizationStatus = "" + + init() { + locationMonitor = backgroundLocationMonitor! + isLocationMonitoringON = locationMonitor.isStarted + authorizationStatus = locationMonitor.currentAuthorizationStatus + locationMonitor.onAuthorizationStatusChange = { [weak self] newStatus in + self?.authorizationStatus = newStatus + } + willCrashDuringNextBackgroundLaunch = locationMonitor.shouldCrashDuringNextBackgroundLaunch + willCrashOnNextBackgroundEvent = locationMonitor.shouldCrashOnNextBackgroundEvent + } + + func startLocationMonitoring() { + locationMonitor.startMonitoring() + isLocationMonitoringON = locationMonitor.isStarted + } + + func stopLocationMonitoring() { + locationMonitor.stopMonitoring() + isLocationMonitoringON = locationMonitor.isStarted + } + + func toggleCrashDuringNextBackgroundLaunch() { + locationMonitor.setCrashDuringNextBackgroundLaunch(!locationMonitor.shouldCrashDuringNextBackgroundLaunch) + willCrashDuringNextBackgroundLaunch = locationMonitor.shouldCrashDuringNextBackgroundLaunch + } + + func toggleCrashOnNextBackgroundEvent() { + locationMonitor.setCrashOnNextBackgroundEvent(!locationMonitor.shouldCrashOnNextBackgroundEvent) + willCrashOnNextBackgroundEvent = locationMonitor.shouldCrashOnNextBackgroundEvent + } +} + +@available(iOS 13, tvOS 13,*) +internal struct DebugBackgroundEventsView: View { + @ObservedObject private var viewModel = DebugBackgroundEventsViewModel() + + var body: some View { + VStack(spacing: 18) { + Text("CLLocationManager") + .font(.headline) + .padding() + Divider() + HStack { + Text("Authorization Status:") + .font(.body).fontWeight(.light) + Spacer() + Text(viewModel.authorizationStatus) + .font(.body) + } + HStack { + Text("Location Monitoring:") + .font(.body).fontWeight(.light) + Spacer() + if #available(iOS 14, tvOS 14, *) { + if viewModel.isLocationMonitoringON { + ProgressView().padding(.trailing, 8) + } + } + Button(viewModel.isLocationMonitoringON ? "STOP" : "START") { + if viewModel.isLocationMonitoringON { + viewModel.stopLocationMonitoring() + } else { + viewModel.startLocationMonitoring() + } + } + } + Divider() + HStack { + Text("Crash during next background launch:").font(.footnote).fontWeight(.light) + Spacer() + Button(viewModel.willCrashDuringNextBackgroundLaunch ? "🔥 ENABLED" : "DISABLED") { + viewModel.toggleCrashDuringNextBackgroundLaunch() + } + } + HStack { + Text("Crash on next background event:").font(.footnote).fontWeight(.light) + Spacer() + Button(viewModel.willCrashOnNextBackgroundEvent ? "🔥 ENABLED" : "DISABLED") { + viewModel.toggleCrashOnNextBackgroundEvent() + } + } + Divider() + Text("Above settings are preserved between application launches, so they are also effective when app is launched in the background due to **significant** location change.") + .font(.footnote) + Spacer() + } + .buttonStyle(DatadogButtonStyle()) + .padding() + } +} + +// MARK - Preview + +@available(iOS 13, tvOS 13,*) +internal struct DebugBackgroundEventsView_Previews: PreviewProvider { + static var previews: some View { + Group { + DebugBackgroundEventsView() + .previewLayout(.fixed(width: 400, height: 500)) + .preferredColorScheme(.light) + DebugBackgroundEventsView() + .previewLayout(.fixed(width: 400, height: 500)) + .preferredColorScheme(.dark) + } + } +} diff --git a/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.h b/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.h new file mode 100644 index 0000000000..008a489030 --- /dev/null +++ b/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.h @@ -0,0 +1,18 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface CrashReportingObjcHelpers : NSObject + +- (void) throwUncaughtNSException; +- (void) dereferenceNullPointer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.m b/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.m new file mode 100644 index 0000000000..1d27f5dfbe --- /dev/null +++ b/Datadog/Example/Debugging/CrashReporting/CrashReportingObjcHelpers.m @@ -0,0 +1,21 @@ +/* + * 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 "CrashReportingObjcHelpers.h" + +@implementation CrashReportingObjcHelpers + +- (void)throwUncaughtNSException { + id object = [NSObject new]; + [(NSDictionary*)object objectForKey:@"foo"]; +} + +- (void)dereferenceNullPointer { + int* pointer = NULL; + *pointer = 1; +} + +@end diff --git a/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift b/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift new file mode 100644 index 0000000000..8c531f96ff --- /dev/null +++ b/Datadog/Example/Debugging/DebugCrashReportingWithRUMViewController.swift @@ -0,0 +1,68 @@ +/* + * 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 DatadogRUM + +class DebugCrashReportingWithRUMViewController: UIViewController { + @IBOutlet weak var rumServiceNameTextField: UITextField! + + override func viewDidLoad() { + super.viewDidLoad() + rumServiceNameTextField.text = serviceName + viewNameTextField.placeholder = viewName + } + + private func crash() { + let objc = CrashReportingObjcHelpers() + objc.throwUncaughtNSException() + } + + // MARK: - Crash after starting RUM session + + @IBOutlet weak var viewNameTextField: UITextField! + + private var viewName: String { + viewNameTextField.text!.isEmpty ? "FooViewController" : viewNameTextField.text! + } + + @IBAction func didTapCrashAfterStartingRUMSession(_ sender: Any) { + (sender as? UIButton)?.disableFor(seconds: 0.5) + + rumMonitor.startView(key: viewName, name: viewName, attributes: [:]) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.crash() + } + } + + // MARK: - Crash before starting RUM session + + @IBAction func didTapCrashBeforeStartingRUMSession(_ sender: Any) { + (sender as? UIButton)?.disableFor(seconds: 0.5) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.crash() + } + } + + // MARK: - OOM Crash + + @IBAction func didTapOOMCrash(_ sender: UIButton) { + DispatchQueue.main.async { + let megaByte = 1_024 * 1_024 + let memoryPageSize = NSPageSize() + let memoryPages = megaByte / memoryPageSize + + while true { + // Allocate one MB and set one element of each memory page to something. + let ptr = UnsafeMutablePointer.allocate(capacity: megaByte) + for i in 0.. Void) { (0..<10).forEach { _ in block() } } + + // MARK: - Stress testing + + var queues: [DispatchQueue] = [] + var loggers: [LoggerProtocol] = [] + + @IBAction func didTapStressTest(_ sender: Any) { + stressTestButton.disableFor(seconds: 10) + + loggers = (0..<5).map { index in + return Logger.create( + with: Logger.Configuration( + name: "stress-logger-\(index)", + networkInfoEnabled: true + ) + ) + } + + queues = (0..<5).map { index in + return DispatchQueue(label: "com.datadoghq.example.stress-testing-queue\(index)") + } + + let endDate = Date(timeIntervalSinceNow: 10) // 10s + zip(loggers, queues).forEach { logger, queue in + keepSendingLogs(on: queue, using: logger, every: 0.01, until: endDate) + } + } + + private func keepSendingLogs(on queue: DispatchQueue, using logger: LoggerProtocol, every timeInterval: TimeInterval, until endDate: Date) { + if Date() < endDate { + queue.asyncAfter(deadline: .now() + timeInterval) { [weak self] in + logger.debug(self?.randomLogMessage() ?? "") + self?.keepSendingLogs(on: queue, using: logger, every: timeInterval, until: endDate) + } + } + } + + private let alphanumerics = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + private func randomLogMessage() -> String { + return String((0..<20).map { _ in alphanumerics.randomElement()! }) + } } diff --git a/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift new file mode 100644 index 0000000000..1545977968 --- /dev/null +++ b/Datadog/Example/Debugging/DebugManualTraceInjectionViewController.swift @@ -0,0 +1,201 @@ +/* + * 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 +import DatadogTrace +import DatadogInternal + +@available(iOS 14, *) +internal class DebugManualTraceInjectionViewController: UIHostingController { + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: DebugManualTraceInjectionView()) + } +} + +private var currentSession: URLSession? = nil + +extension TraceContextInjection { + func toString() -> String { + switch self { + case .all: + return "All" + case .sampled: + return "Sampled" + } + } +} + +@available(iOS 14.0, *) +internal struct DebugManualTraceInjectionView: View { + 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 = "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( + configuration: .ephemeral, + delegate: DDURLSessionDelegate(), + delegateQueue: nil + ) + + var body: some View { + let isButtonDisabled = isRequestPending || spanName.isEmpty || requestURL.isEmpty + + VStack() { + VStack(spacing: 8) { + Text("Trace injection") + .font(.caption.weight(.bold)) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("After tapping \"SEND REQUEST\", a POST request will be sent to the given URL. The request will be traced using the chosen tracing header type and sample rate. A span with specified name will be sent to Datadog.") + .font(.caption.weight(.light)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding() + + Form { + Section(header: Text("Traced URL:")) { + TextField("", text: $requestURL) + } + Section(header: Text("Span name:")) { + TextField("", text: $spanName) + } + 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, + in: 0...100, step: 1, + minimumValueLabel: Text("0"), + maximumValueLabel: Text("100") + ) { + Text("Sample Rate") + } + } + } + + Spacer() + + Button(action: { prepareAndSendRequest() }) { + Text("SEND REQUEST") + .fontWeight(.bold) + .foregroundColor(.white) + } + .frame(maxWidth: .infinity) + .padding() + .background(isButtonDisabled ? Color.gray : Color.datadogPurple) + .cornerRadius(10) + .disabled(isButtonDisabled) + .padding(.horizontal, 8) + .padding(.bottom, 30) + } + } + + private func prepareAndSendRequest() { + guard let url = URL(string: requestURL) else { + print("🔥 POST Request not sent - invalid url: \(requestURL)") + return + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let span = Tracer.shared().startRootSpan(operationName: spanName) + + 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) { + span.finish() + print("✅ Request sent to \(requestURL)") + } + } + + private func send(request: URLRequest, completion: @escaping () -> Void) { + isRequestPending = true + 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 } + } + task.resume() + } +} + +// MARK - Preview + +@available(iOS 14.0, *) + +struct DebugTraceInjectionView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + DebugManualTraceInjectionView() + } + } +} diff --git a/Datadog/Example/Debugging/DebugOTelTracingViewController.swift b/Datadog/Example/Debugging/DebugOTelTracingViewController.swift new file mode 100644 index 0000000000..ae5970e6a7 --- /dev/null +++ b/Datadog/Example/Debugging/DebugOTelTracingViewController.swift @@ -0,0 +1,150 @@ +/* + * 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 DatadogCore +import DatadogTrace +import OpenTelemetryApi + +class DebugOTelTracingViewController: UIViewController { + @IBOutlet weak var serviceNameTextField: UITextField! + @IBOutlet weak var isErrorSegmentedControl: UISegmentedControl! + @IBOutlet weak var singleSpanOperationNameTextField: UITextField! + @IBOutlet weak var singleSpanResourceNameTextField: UITextField! + @IBOutlet weak var sendSingleSpanButton: UIButton! + @IBOutlet weak var complexSpanOperationNameTextField: UITextField! + @IBOutlet weak var sendComplexSpanButton: UIButton! + @IBOutlet weak var sendSpanLinksButton: UIButton! + @IBOutlet weak var consoleTextView: UITextView! + + private let queue1 = DispatchQueue(label: "com.datadoghq.debug-tracing1") + private let queue2 = DispatchQueue(label: "com.datadoghq.debug-tracing2") + private let queue3 = DispatchQueue(label: "com.datadoghq.debug-tracing3") + + override func viewDidLoad() { + super.viewDidLoad() + serviceNameTextField.text = serviceName + hideKeyboardWhenTapOutside() + startDisplayingDebugInfo(in: consoleTextView) + } + + private var isError: Bool { + isErrorSegmentedControl.selectedSegmentIndex == 1 + } + + // MARK: - Sending single span + + private var singleSpanOperationName: String { + singleSpanOperationNameTextField.text!.isEmpty ? "otel single span" : singleSpanOperationNameTextField.text! + } + + private var singleSpanResourceName: String? { + singleSpanResourceNameTextField.text!.isEmpty ? nil : singleSpanResourceNameTextField.text! + } + + @IBAction func didTapSendSingleSpan(_ sender: Any) { + sendSingleSpanButton.disableFor(seconds: 0.5) + + let spanName = singleSpanOperationName + let resourceName = singleSpanResourceName + let isError = self.isError + + queue1.async { + let span = otelTracer.spanBuilder(spanName: spanName) + .startSpan() + if let resourceName = resourceName { + span.setAttribute(key: SpanTags.resource, value: resourceName) + } + if isError { + // To only mark the span as an error, use the Open Tracing `error` tag: + // span.setTag(key: "error", value: true) + span.status = .error(description: "error description") + } + wait(seconds: 1) + span.end() + } + } + + // MARK: - Sending complex span + + private var complexSpanOperationName: String { + complexSpanOperationNameTextField.text!.isEmpty ? "otel complex span" : complexSpanOperationNameTextField.text! + } + + @IBAction func didTapSendComplexSpan(_ sender: Any) { + sendComplexSpanButton.disableFor(seconds: 0.5) + + let spanName = complexSpanOperationName + + queue1.async { [weak self] in + guard let self = self else { return } + + let rootSpan = otelTracer + .spanBuilder(spanName: spanName) + .setActive(true) + .startSpan() + wait(seconds: 0.5) + + self.queue2.sync { + let child1 = otelTracer.spanBuilder(spanName: "otel child operation 1") + .startSpan() + wait(seconds: 0.5) + child1.end() + + wait(seconds: 0.1) + + let child2 = otelTracer + .spanBuilder(spanName: "otel child operation 2") + .setParent(rootSpan) + .startSpan() + wait(seconds: 0.5) + + self.queue3.sync { + let grandChild = otelTracer + .spanBuilder(spanName: "otel grandchild operation") + .setParent(child2) + .startSpan() + wait(seconds: 1) + grandChild.end() + } + + OpenTelemetry.instance.contextProvider.setActiveSpan(child2) + let child2Child = otelTracer.spanBuilder(spanName: "otel child2 child") + .startSpan() + wait(seconds: 0.5) + child2Child.end() + + child2.end() + } + + wait(seconds: 0.5) + rootSpan.end() + } + } + + // MARK: - Sending span links + + @IBAction func didTabSendSpanLinks(_ sender: Any) { + sendSpanLinksButton.disableFor(seconds: 1) + queue1.async { + let span1 = otelTracer.spanBuilder(spanName: "span 1") + .startSpan() + wait(seconds: 0.5) + + let span2 = otelTracer.spanBuilder(spanName: "span 2") + .addLink(spanContext: span1.context) + .startSpan() + wait(seconds: 0.5) + span2.end() + + span1.end() + } + } +} + +private func wait(seconds: TimeInterval) { + Thread.sleep(forTimeInterval: seconds) +} diff --git a/Datadog/Example/Debugging/DebugRUMSessionViewController.swift b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift new file mode 100644 index 0000000000..73cb64e9e4 --- /dev/null +++ b/Datadog/Example/Debugging/DebugRUMSessionViewController.swift @@ -0,0 +1,389 @@ +/* + * 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 +import DatadogRUM +import DatadogTrace + +@available(iOS 13, *) +internal class DebugRUMSessionViewController: UIHostingController { + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: DebugRUMSessionView()) + } +} + +private enum SessionItemType { + case view + case resource + case action + case error +} + +@available(iOS 13.0, *) +private class DebugRUMSessionViewModel: ObservableObject { + struct SessionItem: Identifiable { + let label: String + let type: SessionItemType + var isPending: Bool + var stopAction: (() -> Void)? + + var id: UUID = UUID() + } + + @Published var sessionItems: [SessionItem] = [] { + didSet { updateSessionID() } + } + @Published var sessionID: String = "" + + @Published var viewKey: String = "" + @Published var actionName: String = "" + @Published var errorMessage: String = "" + @Published var resourceKey: String = "" + + @Published var logMessage: String = "" + @Published var spanOperationName: String = "" + @Published var instrumentedRequestURL: String = "https://api.shopist.io/checkout.json" + + 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, + type: .view, + isPending: true, + stopAction: { [weak self] in + self?.modifySessionItem(type: .view, label: key) { mutableSessionItem in + mutableSessionItem.isPending = false + mutableSessionItem.stopAction = nil + RUMMonitor.shared().stopView(key: key) + } + } + ) + ) + + self.viewKey = "" + } + + func addAction() { + guard !actionName.isEmpty else { + return + } + + RUMMonitor.shared().addAction(type: .custom, name: actionName) + sessionItems.append( + SessionItem(label: actionName, type: .action, isPending: false, stopAction: nil) + ) + + self.actionName = "" + } + + func addError() { + guard !errorMessage.isEmpty else { + return + } + + RUMMonitor.shared().addError(message: errorMessage) + sessionItems.append( + SessionItem(label: errorMessage, type: .error, isPending: false, stopAction: nil) + ) + + self.errorMessage = "" + } + + func startResource() { + guard !resourceKey.isEmpty else { + return + } + + let key = self.resourceKey + RUMMonitor.shared().startResource(resourceKey: key, url: mockURL()) + sessionItems.append( + SessionItem( + label: key, + type: .resource, + isPending: true, + stopAction: { [weak self] in + self?.modifySessionItem(type: .resource, label: key) { mutableSessionItem in + mutableSessionItem.isPending = false + mutableSessionItem.stopAction = nil + RUMMonitor.shared().stopResource(resourceKey: key, statusCode: nil, kind: .other) + } + } + ) + ) + + self.resourceKey = "" + } + + func sendLog() { + logger.debug(logMessage) + logMessage = "" + } + + func sendSpan() { + let span = Tracer.shared().startRootSpan(operationName: spanOperationName, tags: [:]) + Thread.sleep(forTimeInterval: 0.1) + span.finish() + spanOperationName = "" + } + + func sendPOSTRequest() { + guard let url = URL(string: instrumentedRequestURL) else { + print("🔥 POST Request not sent - invalid url: \(instrumentedRequestURL)") + return + } + guard let host = url.host else { + print("🔥 POST Request not sent - invalid url host: \(instrumentedRequestURL)") + return + } + + let delegate = DDURLSessionDelegate(additionalFirstPartyHosts: [host]) + let session = URLSession(configuration: .ephemeral, delegate: delegate, delegateQueue: nil) + + var request = URLRequest(url: url) + request.httpMethod = "POST" + let task = session.dataTask(with: request) { _, _, error in + if let error = error { + print("🌍🔥 POST \(url) completed with network error: \(error)") + } else { + print("🌍 POST \(url) sent successfully") + } + } + task.resume() + + urlSessions.append(session) // keep session + } + + func stopSession() { + RUMMonitor.shared().stopSession() + sessionItems = [] + } + + // MARK: - Private + + private func modifySessionItem(type: SessionItemType, label: String, change: (inout SessionItem) -> Void) { + sessionItems = sessionItems.map { item in + var item = item + if item.type == type, item.label == label { + change(&item) + } + return item + } + } + + 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, *) +internal struct DebugRUMSessionView: View { + @ObservedObject private var viewModel = DebugRUMSessionViewModel() + + var body: some View { + VStack() { + Group { + Text("RUM Session") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.bold)) + Text("Debug RUM Session by creating events manually:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "RUM View", placeholder: "view key", accent: .rumViewColor, value: $viewModel.viewKey + ) + Button("START") { viewModel.startView() } + } + HStack { + FormItemView( + title: "RUM Action", placeholder: "name", accent: .rumActionColor, value: $viewModel.actionName + ) + Button("ADD") { viewModel.addAction() } + } + HStack { + FormItemView( + title: "RUM Error", placeholder: "message", accent: .rumErrorColor, value: $viewModel.errorMessage + ) + Button("ADD") { viewModel.addError() } + } + HStack { + FormItemView( + title: "RUM Resource", placeholder: "key", accent: .rumResourceColor, value: $viewModel.resourceKey + ) + Button("START") { viewModel.startResource() } + } + HStack { + Button("STOP SESSION") { viewModel.stopSession() } + Spacer() + } + Divider() + } + Group { + Text("Bundling Logs and Spans") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.bold)) + Text("Debug bundling Logs and Spans with RUM Session by sending them manually while the session is active.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "Log", placeholder: "log message", accent: .gray, value: $viewModel.logMessage + ) + Button("Send") { viewModel.sendLog() } + } + HStack { + FormItemView( + title: "Span", placeholder: "span name", accent: .gray, value: $viewModel.spanOperationName + ) + Button("Send") { viewModel.sendSpan() } + } + Text("Send 1st party request with instrumented URLSession:") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption.weight(.light)) + HStack { + FormItemView( + title: "POST Request", placeholder: "request url", accent: .gray, value: $viewModel.instrumentedRequestURL + ) + Button("Send") { viewModel.sendPOSTRequest() } + } + Divider() + } + Group { + 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()) + .padding(4) + } + .listStyle(PlainListStyle()) + } + } + .buttonStyle(DatadogButtonStyle()) + .padding() + } +} + +@available(iOS 13.0, *) +private struct FormItemView: View { + let title: String + let placeholder: String + let accent: Color + + @Binding var value: String + + var body: some View { + HStack { + Text(title) + .bold() + .font(.system(size: 10)) + .padding(4) + .background(accent) + .foregroundColor(Color.white) + .cornerRadius(4) + TextField(placeholder, text: $value) + .font(.system(size: 12)) + .padding(4) + .background(Color(UIColor.secondarySystemFill)) + .cornerRadius(4) + } + .padding(4) + .background(Color(UIColor.systemFill)) + .foregroundColor(Color.secondary) + .cornerRadius(4) + } +} + +@available(iOS 13.0, *) +private struct SessionItemView: View { + let item: DebugRUMSessionViewModel.SessionItem + + var body: some View { + HStack() { + HStack() { + Text(label(for: item.type)) + .bold() + .font(.system(size: 10)) + .padding(4) + .background(color(for: item.type)) + .foregroundColor(Color.white) + .cornerRadius(4) + Text(item.label) + .bold() + .font(.system(size: 14)) + Spacer() + } + .padding(4) + .frame(maxWidth: .infinity) + .background(Color(UIColor.systemFill)) + .foregroundColor(Color.secondary) + .cornerRadius(4) + + if item.isPending { + Button("STOP") { item.stopAction?() } + } + } + } + + private func color(for sessionItemType: SessionItemType) -> Color { + switch sessionItemType { + case .view: return .rumViewColor + case .resource: return .rumResourceColor + case .action: return .rumActionColor + case .error: return .rumErrorColor + } + } + + private func label(for sessionItemType: SessionItemType) -> String { + switch sessionItemType { + case .view: return "RUM View" + case .resource: return "RUM Resource" + case .action: return "RUM Action" + case .error: return "RUM Error" + } + } +} + +// MARK - Preview + +@available(iOS 13.0, *) +struct DebugRUMSessionViewController_Previews: PreviewProvider { + static var previews: some View { + Group { + DebugRUMSessionView() + .previewLayout(.fixed(width: 400, height: 500)) + .preferredColorScheme(.light) + DebugRUMSessionView() + .previewLayout(.fixed(width: 400, height: 500)) + .preferredColorScheme(.dark) + } + } +} diff --git a/Datadog/Example/Debugging/DebugRUMViewController.swift b/Datadog/Example/Debugging/DebugRUMViewController.swift new file mode 100644 index 0000000000..dc65bfd189 --- /dev/null +++ b/Datadog/Example/Debugging/DebugRUMViewController.swift @@ -0,0 +1,265 @@ +/* + * 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 DatadogRUM +import DatadogCore +import DatadogInternal + +class DebugRUMViewController: UIViewController { + @IBOutlet weak var rumServiceNameTextField: UITextField! + + private var simulatedViewControllers: [UIViewController] = [] + + override func viewDidLoad() { + super.viewDidLoad() + rumServiceNameTextField.text = serviceName + hideKeyboardWhenTapOutside() + + viewURLTextField.placeholder = viewURL + actionViewURLTextField.placeholder = actionViewURL + actionTypeTextField.placeholder = RUMActionType.default.toString + resourceViewURLTextField.placeholder = resourceViewURL + resourceURLTextField.placeholder = resourceURL + errorViewURLTextField.placeholder = errorViewURL + errorMessageTextField.placeholder = errorMessage + } + + // MARK: - View Event + + @IBOutlet weak var viewURLTextField: UITextField! + @IBOutlet weak var sendViewEventButton: UIButton! + + private var viewURL: String { + viewURLTextField.text!.isEmpty ? "FooViewController" : viewURLTextField.text! + } + + @IBAction func didTapSendViewEvent(_ sender: Any) { + let viewController = createUIViewControllerSubclassInstance(named: viewURL) + rumMonitor.startView(viewController: viewController) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + rumMonitor.stopView(viewController: viewController) + } + simulatedViewControllers.append(viewController) + sendViewEventButton.disableFor(seconds: 0.5) + } + + // MARK: - Action Event + + @IBOutlet weak var actionViewURLTextField: UITextField! + @IBOutlet weak var actionTypeTextField: UITextField! + @IBOutlet weak var sendActionEventButton: UIButton! + + private var actionViewURL: String { + actionViewURLTextField.text!.isEmpty ? "FooViewController" : actionViewURLTextField.text! + } + + private var actionType: RUMActionType { + let actionType = actionTypeTextField.text.flatMap { RUMActionType(string: $0) } + return actionType ?? RUMActionType.default + } + + @IBAction func didTapSendActionEvent(_ sender: Any) { + let viewController = createUIViewControllerSubclassInstance(named: actionViewURL) + rumMonitor.startView(viewController: viewController) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + rumMonitor.addAction(type: self.actionType, name: (sender as! UIButton).currentTitle!) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + rumMonitor.stopView(viewController: viewController) + } + simulatedViewControllers.append(viewController) + sendActionEventButton.disableFor(seconds: 0.5) + + if actionType.toString != actionTypeTextField.text { // if `actionType` was replaced with allowed type + if !actionTypeTextField.text!.isEmpty { // when not using placeholder + actionTypeTextField.text = actionType.toString + } + } + } + + // MARK: - Resource Event + + @IBOutlet weak var resourceTypeSegment: UISegmentedControl! + @IBOutlet weak var resourceViewURLTextField: UITextField! + @IBOutlet weak var resourceURLTextField: UITextField! + @IBOutlet weak var sendResourceEventButton: UIButton! + + private var resourceViewURL: String { + resourceViewURLTextField.text!.isEmpty ? "FooViewController" : resourceViewURLTextField.text! + } + + private var resourceURL: String { + resourceURLTextField.text!.isEmpty ? "https://api.shopist.io/checkout.json" : resourceURLTextField.text! + } + + @IBAction func didTapSendResourceEvent(_ sender: Any) { + let viewController = createUIViewControllerSubclassInstance(named: resourceViewURL) + rumMonitor.startView(viewController: viewController) + + if resourceTypeSegment.selectedSegmentIndex == 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.sendManualResource(completeAfter: 0.2) + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.sendInstrumentedResource() + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + rumMonitor.stopView(viewController: viewController) + } + simulatedViewControllers.append(viewController) + sendResourceEventButton.disableFor(seconds: 0.5) + } + + private func sendManualResource(completeAfter time: TimeInterval) { + let request = URLRequest(url: URL(string: self.resourceURL)!) + rumMonitor.startResource( + resourceKey: "/resource/1", + request: request + ) + DispatchQueue.main.asyncAfter(deadline: .now() + time) { + rumMonitor.stopResource( + resourceKey: "/resource/1", + response: HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "image/png"] + )! + ) + } + } + + private lazy var instrumentedSession: URLSession = { + class InstrumentedDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {} + URLSessionInstrumentation.enable(with: .init(delegateClass: InstrumentedDelegate.self)) + return URLSession(configuration: .ephemeral, delegate: InstrumentedDelegate(), delegateQueue: .main) + }() + + private func sendInstrumentedResource() { + var request = URLRequest(url: URL(string: self.resourceURL)!) + request.httpMethod = "POST" + instrumentedSession.dataTask(with: request) { _, _, error in + if let error = error { + print("🌍🔥 POST \(request.url!.absoluteString) completed with network error: \(error)") + } else { + print("🌍 POST \(request.url!.absoluteString) sent successfully") + } + }.resume() + } + + // MARK: - Error Event + + @IBOutlet weak var errorViewURLTextField: UITextField! + @IBOutlet weak var errorMessageTextField: UITextField! + @IBOutlet weak var sendErrorEventButton: UIButton! + + private var errorViewURL: String { + errorViewURLTextField.text!.isEmpty ? "FooViewController" : errorViewURLTextField.text! + } + + private var errorMessage: String { + errorMessageTextField.text!.isEmpty ? "Error message" : errorMessageTextField.text! + } + + @IBAction func didTapSendErrorEvent(_ sender: Any) { + let viewController = createUIViewControllerSubclassInstance(named: errorViewURL) + rumMonitor.startView(viewController: viewController) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + rumMonitor.addError(message: self.errorMessage, source: .source) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + rumMonitor.stopView(viewController: viewController) + } + 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 + +/// Creates an instance of `UIViewController` subclass with a given name. +private func createUIViewControllerSubclassInstance(named viewControllerClassName: String) -> UIViewController { + let theClass: AnyClass = NSClassFromString(viewControllerClassName) ?? { + let cls: AnyClass + cls = objc_allocateClassPair(UIViewController.classForCoder(), viewControllerClassName, 0)! + objc_registerClassPair(cls) + return cls + }() + return theClass.alloc() as! UIViewController +} + +extension RUMActionType { + init(string: String) { + switch string { + case "tap": self = .tap + case "scroll": self = .scroll + case "swipe": self = .swipe + case "custom": self = .custom + default: self = RUMActionType.default + } + } + + var toString: String { + switch self { + case .tap: return "tap" + case .click: return "click" + case .scroll: return "scroll" + case .swipe: return "swipe" + case .custom: return "custom" + } + } + + static var `default`: RUMActionType = .custom +} diff --git a/Datadog/Example/Debugging/DebugTracingViewController.swift b/Datadog/Example/Debugging/DebugTracingViewController.swift index 393c5c6ce7..fe5b1a0a8e 100644 --- a/Datadog/Example/Debugging/DebugTracingViewController.swift +++ b/Datadog/Example/Debugging/DebugTracingViewController.swift @@ -1,11 +1,12 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import UIKit -import Datadog +import DatadogCore +import DatadogTrace class DebugTracingViewController: UIViewController { @IBOutlet weak var serviceNameTextField: UITextField! @@ -23,7 +24,7 @@ class DebugTracingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - serviceNameTextField.text = appConfig.serviceName + serviceNameTextField.text = serviceName hideKeyboardWhenTapOutside() startDisplayingDebugInfo(in: consoleTextView) } @@ -50,9 +51,9 @@ class DebugTracingViewController: UIViewController { let isError = self.isError queue1.async { - let span = Global.sharedTracer.startSpan(operationName: spanName) + let span = tracer.startSpan(operationName: spanName) if let resourceName = resourceName { - span.setTag(key: DDTags.resource, value: resourceName) + span.setTag(key: SpanTags.resource, value: resourceName) } if isError { // To only mark the span as an error, use the Open Tracing `error` tag: @@ -88,21 +89,21 @@ class DebugTracingViewController: UIViewController { queue1.async { [weak self] in guard let self = self else { return } - let rootSpan = Global.sharedTracer.startSpan(operationName: spanName) + let rootSpan = tracer.startSpan(operationName: spanName) wait(seconds: 0.5) self.queue2.sync { - let child1 = Global.sharedTracer.startSpan(operationName: "child operation 1", childOf: rootSpan.context) + let child1 = tracer.startSpan(operationName: "child operation 1", childOf: rootSpan.context) wait(seconds: 0.5) child1.finish() wait(seconds: 0.1) - let child2 = Global.sharedTracer.startSpan(operationName: "child operation 2", childOf: rootSpan.context) + let child2 = tracer.startSpan(operationName: "child operation 2", childOf: rootSpan.context) wait(seconds: 0.5) self.queue3.sync { - let grandChild = Global.sharedTracer.startSpan(operationName: "grandchild operation", childOf: child2.context) + let grandChild = tracer.startSpan(operationName: "grandchild operation", childOf: child2.context) wait(seconds: 1) grandChild.finish() } @@ -117,5 +118,5 @@ class DebugTracingViewController: UIViewController { } private func wait(seconds: TimeInterval) { - Thread.sleep(forTimeInterval: 0.5) + Thread.sleep(forTimeInterval: seconds) } diff --git a/Datadog/Example/Debugging/DebugWebviewViewController.swift b/Datadog/Example/Debugging/DebugWebviewViewController.swift new file mode 100644 index 0000000000..507371a16c --- /dev/null +++ b/Datadog/Example/Debugging/DebugWebviewViewController.swift @@ -0,0 +1,119 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import UIKit +import WebKit +import DatadogRUM +import DatadogWebViewTracking + +class DebugWebviewViewController: UIViewController { + @IBOutlet weak var rumServiceNameTextField: UITextField! + + /// When `true`, a native RUM Session between `DebugWebviewViewController` and `WebviewViewController` will be tracked. + private var useNativeRUMSession = false + + override func viewDidLoad() { + super.viewDidLoad() + rumServiceNameTextField.text = serviceName + webviewURLTextField.placeholder = webviewURL + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if useNativeRUMSession { + RUMMonitor.shared() + .startView(viewController: self) + } + } + + override func viewDidDisappear(_ animated: Bool) { + if useNativeRUMSession { + RUMMonitor.shared() + .stopView(viewController: self) + } + } + + // MARK: - Starting native RUM session + + @IBAction func didTapStartNativeRUMSession(_ sender: Any) { + useNativeRUMSession = true + RUMMonitor.shared() + .startView(viewController: self) + } + + // MARK: - Starting webview + + @IBOutlet weak var webviewURLTextField: UITextField! + + private var webviewURL: String { + guard let text = webviewURLTextField.text, !text.isEmpty else { + return "https://datadoghq.dev/browser-sdk-test-playground/webview.html" + } + return text + } + + @IBAction func didTapOpenURLInWebview(_ sender: Any) { + if let url = URL(string: webviewURL), url.scheme != nil, url.host != nil { // basic validation + webviewURLTextField.textColor = .black + let webviewVC = WebviewViewController() + + webviewVC.request = URLRequest( + url: url, + cachePolicy: .reloadIgnoringLocalCacheData // no cache + ) + webviewVC.navigationItem.title = "\(url.absoluteString)" + webviewVC.useNativeRUMSession = useNativeRUMSession + + show(webviewVC, sender: nil) + } else { + webviewURLTextField.textColor = .red + } + } +} + +// MARK: - Webview view controller + +class WebviewViewController: UIViewController { + /// The request to load in webview + var request: URLRequest! + /// When `true`, a native RUM Session between `DebugWebviewViewController` and `WebviewViewController` will be tracked. + var useNativeRUMSession: Bool! + + private var webView: WKWebView! + + override func viewDidLoad() { + super.viewDidLoad() + + let controller = WKUserContentController() + let config = WKWebViewConfiguration() + config.userContentController = controller + webView = WKWebView(frame: UIScreen.main.bounds, configuration: config) + + WebViewTracking.enable(webView: webView) + + view.addSubview(webView) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + webView.load(request) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if useNativeRUMSession { + RUMMonitor.shared() + .startView(viewController: self) + } + } + + override func viewDidDisappear(_ animated: Bool) { + if useNativeRUMSession { + RUMMonitor.shared() + .stopView(viewController: self) + } + } +} diff --git a/Datadog/Example/Debugging/Helpers/SwiftUI.swift b/Datadog/Example/Debugging/Helpers/SwiftUI.swift new file mode 100644 index 0000000000..e06c1c384c --- /dev/null +++ b/Datadog/Example/Debugging/Helpers/SwiftUI.swift @@ -0,0 +1,43 @@ +/* + * 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 13, tvOS 13,*) +extension Color { + /// Datadog purple. + static var datadogPurple: Color { + return Color(UIColor(red: 99/256, green: 44/256, blue: 166/256, alpha: 1)) + } + + static var rumViewColor: Color { + return Color(UIColor(red: 0/256, green: 107/256, blue: 194/256, alpha: 1)) + } + + static var rumResourceColor: Color { + return Color(UIColor(red: 113/256, green: 184/256, blue: 231/256, alpha: 1)) + } + + static var rumActionColor: Color { + return Color(UIColor(red: 150/256, green: 95/256, blue: 204/256, alpha: 1)) + } + + static var rumErrorColor: Color { + return Color(UIColor(red: 235/256, green: 54/256, blue: 7/256, alpha: 1)) + } +} + +@available(iOS 13, tvOS 13,*) +internal struct DatadogButtonStyle: ButtonStyle { + func makeBody(configuration: DatadogButtonStyle.Configuration) -> some View { + return configuration.label + .font(.system(size: 12, weight: .medium)) + .padding(6) + .background(Color.datadogPurple) + .foregroundColor(.white) + .cornerRadius(6) + } +} diff --git a/Datadog/Example/Environment.swift b/Datadog/Example/Environment.swift new file mode 100644 index 0000000000..25efbd2d90 --- /dev/null +++ b/Datadog/Example/Environment.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 Foundation + +internal struct Environment { + /// Launch arguments shared between UITests and Example targets. + struct Argument { + static let isRunningUnitTests = "IS_RUNNING_UNIT_TESTS" + static let isRunningUITests = "IS_RUNNING_UI_TESTS" + } + + struct InfoPlistKey { + static let clientToken = "DatadogClientToken" + static let rumApplicationID = "RUMApplicationID" + + static let customLogsURL = "CustomLogsURL" + static let customTraceURL = "CustomTraceURL" + static let customRUMURL = "CustomRUMURL" + } + + // MARK: - Launch Arguments + + static func isRunningUnitTests() -> Bool { + return ProcessInfo.processInfo.arguments.contains(Argument.isRunningUnitTests) + } + + static func isRunningUITests() -> Bool { + return ProcessInfo.processInfo.arguments.contains(Argument.isRunningUITests) + } + + /// If running `Example` in interactive, debug mode (launching it with 'Run' in Xcode or by tapping on the app icon). + static func isRunningInteractive() -> Bool { + return !isRunningUITests() && !isRunningUnitTests() + } + + // MARK: - Info.plist + + static func readClientToken() -> String { + guard let clientToken = Bundle.main.infoDictionary?[InfoPlistKey.clientToken] as? String, !clientToken.isEmpty else { + fatalError(""" + ✋⛔️ Cannot read `\(InfoPlistKey.clientToken)` from `Info.plist` dictionary. + Please update `Datadog.xcconfig` in the repository root with your own + client token obtained on datadoghq.com. + You might need to run `Product > Clean Build Folder` before retrying. + """) + } + return clientToken + } + + static func readRUMApplicationID() -> String { + guard let rumApplicationID = Bundle.main.infoDictionary![InfoPlistKey.rumApplicationID] as? String, !rumApplicationID.isEmpty else { + fatalError(""" + ✋⛔️ Cannot read `\(InfoPlistKey.rumApplicationID)` from `Info.plist` dictionary. + Please update `Datadog.xcconfig` in the repository root with your own + RUM application id obtained on datadoghq.com. + You might need to run `Product > Clean Build Folder` before retrying. + """) + } + return rumApplicationID + } + + static func readCustomLogsURL() -> URL? { + if let customLogsURL = Bundle.main.infoDictionary![InfoPlistKey.customLogsURL] as? String, + !customLogsURL.isEmpty { + return URL(string: "https://\(customLogsURL)") + } + return nil + } + + static func readCustomTraceURL() -> URL? { + if let customTraceURL = Bundle.main.infoDictionary![InfoPlistKey.customTraceURL] as? String, + !customTraceURL.isEmpty { + return URL(string: "https://\(customTraceURL)") + } + return nil + } + + static func readCustomRUMURL() -> URL? { + if let customRUMURL = Bundle.main.infoDictionary![InfoPlistKey.customRUMURL] as? String, + !customRUMURL.isEmpty { + return URL(string: "https://\(customRUMURL)") + } + return nil + } +} diff --git a/Datadog/Example/ExampleAppDelegate.swift b/Datadog/Example/ExampleAppDelegate.swift new file mode 100644 index 0000000000..045d08416e --- /dev/null +++ b/Datadog/Example/ExampleAppDelegate.swift @@ -0,0 +1,145 @@ +/* +* 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 DatadogCore +import DatadogLogs +import DatadogTrace +import DatadogRUM +import DatadogCrashReporting +import OpenTelemetryApi + +let serviceName = "ios-sdk-example-app" + +var logger: LoggerProtocol! +var tracer: OTTracer { Tracer.shared() } +var rumMonitor: RUMMonitorProtocol { RUMMonitor.shared() } +var otelTracer: OpenTelemetryApi.Tracer { + OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) +} + +@UIApplicationMain +class ExampleAppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + if Environment.isRunningUnitTests() { + return false + } + + // Initialize Datadog SDK + Datadog.initialize( + with: Datadog.Configuration( + clientToken: Environment.readClientToken(), + env: "tests", + service: serviceName, + batchSize: .small, + uploadFrequency: .frequent + ), + trackingConsent: .granted + ) + + // Set user information + Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com", extraInfo: ["key-extraUserInfo": "value-extraUserInfo"]) + + // Enable Logs + Logs.enable( + with: Logs.Configuration( + customEndpoint: Environment.readCustomLogsURL() + ) + ) + + // Enable Crash Reporting + CrashReporting.enable() + + // Set highest verbosity level to see debugging logs from the SDK + Datadog.verbosityLevel = .debug + + // Enable Trace + Trace.enable( + with: Trace.Configuration( + tags: ["testing-tag": "my-value"], + networkInfoEnabled: true, + customEndpoint: Environment.readCustomTraceURL() + ) + ) + + // Enable RUM + RUM.enable( + with: RUM.Configuration( + applicationID: Environment.readRUMApplicationID(), + urlSessionTracking: .init( + resourceAttributesProvider: { req, resp, data, err in + print("⭐️ [Attributes Provider] data: \(String(describing: data))") + return [:] + } + ), + trackBackgroundEvents: true, + trackWatchdogTerminations: true, + customEndpoint: Environment.readCustomRUMURL(), + telemetrySampleRate: 100 + ) + ) + RUMMonitor.shared().debug = true + + // Register Trace Provider + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider() + ) + Logs.addAttribute(forKey: "testing-attribute", value: "my-value") + + // Create Logger + logger = Logger.create( + with: Logger.Configuration( + name: "logger-name", + networkInfoEnabled: true, + consoleLogFormat: .shortWith(prefix: "[iOS App] ") + ) + ) + + logger.addAttribute(forKey: "device-model", value: UIDevice.current.model) + + #if DEBUG + logger.addTag(withKey: "build_configuration", value: "debug") + #else + logger.addTag(withKey: "build_configuration", value: "release") + #endif + + // Launch initial screen depending on the launch configuration + #if os(iOS) + let storyboard = UIStoryboard(name: "Main iOS", bundle: nil) + launch(storyboard: storyboard) + #endif + + #if !os(tvOS) + // Instantiate location monitor if the Example app is run in interactive mode. This will + // enable background location tracking if it was started in previous session. + if Environment.isRunningInteractive() { + backgroundLocationMonitor = BackgroundLocationMonitor(rum: rumMonitor) + } + #endif + + return true + } + + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + if Environment.isRunningInteractive() { + installConsoleOutputInterceptor() + } + return true + } + + func launch(storyboard: UIStoryboard) { + if window == nil { + window = UIWindow(frame: UIScreen.main.bounds) + window?.makeKeyAndVisible() + } + window?.rootViewController = storyboard.instantiateInitialViewController()! + } +} diff --git a/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.swift b/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.swift deleted file mode 100644 index 2f4e6e398c..0000000000 --- a/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.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-2020 Datadog, Inc. - */ - -import UIKit - -internal class SendLogsFixtureViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - - // Send logs - logger.addTag(withKey: "tag1", value: "tag-value") - logger.add(tag: "tag2") - - logger.addAttribute(forKey: "logger-attribute1", value: "string value") - logger.addAttribute(forKey: "logger-attribute2", value: 1_000) - logger.addAttribute(forKey: "some-url", value: URL(string: "https://example.com/image.png")!) - - logger.debug("debug message", attributes: ["attribute": "value"]) - logger.info("info message", attributes: ["attribute": "value"]) - logger.notice("notice message", attributes: ["attribute": "value"]) - logger.warn("warn message", attributes: ["attribute": "value"]) - logger.error("error message", attributes: ["attribute": "value"]) - logger.critical("critical message", attributes: ["attribute": "value"]) - } -} diff --git a/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift b/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift deleted file mode 100644 index 77e840f642..0000000000 --- a/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift +++ /dev/null @@ -1,86 +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-2020 Datadog, Inc. - */ - -import UIKit -import Datadog - -internal class SendTracesFixtureViewController: UIViewController { - private let backgroundQueue = DispatchQueue(label: "background-queue") - - /// Traces view appearing - private var viewAppearingSpan: OTSpan! - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - viewAppearingSpan = tracer.startSpan(operationName: "view appearing") - - // Set `class: SendTracesFixtureViewController` baggage item on the root span, so it will be propagated to all child spans. - viewAppearingSpan.setBaggageItem(key: "class", value: "\(type(of: self))") - - let dataDownloadingSpan = tracer.startSpan( - operationName: "data downloading", - childOf: viewAppearingSpan.context - ) - dataDownloadingSpan.setTag(key: "data.kind", value: "image") - dataDownloadingSpan.setTag(key: "data.url", value: URL(string: "https://example.com/image.png")!) - dataDownloadingSpan.setTag(key: DDTags.resource, value: "GET /image.png") - - // Step #1: Manual tracing with complex hierarchy - downloadSomeData { [weak self] data in - // Simulate logging download progress - dataDownloadingSpan.log( - fields: [ - OTLogFields.message: "download progress", - "progress": 0.99 - ] - ) - - dataDownloadingSpan.finish() - guard let self = self else { return } - - let dataPresentationSpan = tracer.startSpan( - operationName: "data presentation", - childOf: self.viewAppearingSpan.context - ) - self.present(data: data) - dataPresentationSpan.setTag(key: OTTags.error, value: true) - dataPresentationSpan.finish() - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - viewAppearingSpan.finish() - - // Send requests which will be automatically traced as tracing auto-instrumentation is enabled - let url = currentAppConfig().arbitraryNetworkURL - let request = currentAppConfig().arbitraryNetworkRequest - let dnsErrorURL = URL(string: "https://foo.bar")! - // Step #2: Auto-instrumentated request with URL to succeed - URLSession.shared.dataTask(with: url) { _, _, _ in - // Step #3: Auto-instrumentated request with Request to fail - URLSession.shared.dataTask(with: request) { _, _, _ in - // Step #4: Auto-instrumentated request to return NSError - URLSession.shared.dataTask(with: dnsErrorURL) { _, _, _ in }.resume() - }.resume() - }.resume() - } - - /// Simulates doing an asynchronous work with completion. - private func downloadSomeData(completion: @escaping (Data) -> Void) { - backgroundQueue.async { - Thread.sleep(forTimeInterval: 0.3) - DispatchQueue.main.async { completion(Data()) } - } - } - - /// Simulates presenting some data. - private func present(data: Data) { - Thread.sleep(forTimeInterval: 0.06) - } -} diff --git a/Datadog/Example/Utils/ConsoleOutputInterceptor.swift b/Datadog/Example/Utils/ConsoleOutputInterceptor.swift index 3086f56c7a..2aac031e33 100644 --- a/Datadog/Example/Utils/ConsoleOutputInterceptor.swift +++ b/Datadog/Example/Utils/ConsoleOutputInterceptor.swift @@ -1,14 +1,12 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ #if DEBUG -/// Below `@testable import` is only for SDK debug purposes, to easily override internal `consolePrint` function and display -/// the output of the `Logger` in UI. Should be never used in client's application code. -@testable import Datadog +import DatadogInternal import UIKit class ConsoleOutputInterceptor { @@ -24,7 +22,7 @@ class ConsoleOutputInterceptor { consolePrint = self.process } - private func process(newLog: String) { + private func process(newLog: String, level: CoreLoggerLevel) { // send to debugger console: print(newLog) 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/Example/Utils/UIButton+Disabling.swift b/Datadog/Example/Utils/UIButton+Disabling.swift index 415019d067..4ed5328bd4 100644 --- a/Datadog/Example/Utils/UIButton+Disabling.swift +++ b/Datadog/Example/Utils/UIButton+Disabling.swift @@ -1,25 +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-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import UIKit extension UIButton { func disableFor(seconds: TimeInterval) { + let completion = disableUntilCompletion() + DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { + completion() + } + } + + func disableUntilCompletion() -> () -> Void { let originalBackgroundColor = self.backgroundColor self.isEnabled = false - if #available(iOS 13.0, *) { - self.backgroundColor = .systemGray4 - } else { - self.backgroundColor = .systemGray - } + self.backgroundColor = .systemGray - DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in - self?.isEnabled = true - self?.backgroundColor = originalBackgroundColor + return { + self.isEnabled = true + self.backgroundColor = originalBackgroundColor } } } diff --git a/Datadog/Example/Utils/UIViewController+KeyboardControlling.swift b/Datadog/Example/Utils/UIViewController+KeyboardControlling.swift index 109b7f13a8..ccf30638f1 100644 --- a/Datadog/Example/Utils/UIViewController+KeyboardControlling.swift +++ b/Datadog/Example/Utils/UIViewController+KeyboardControlling.swift @@ -1,7 +1,7 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import UIKit diff --git a/Datadog/IntegrationUnitTests/CrashReporting/GeneratingBacktraceTests.swift b/Datadog/IntegrationUnitTests/CrashReporting/GeneratingBacktraceTests.swift new file mode 100644 index 0000000000..bc15124818 --- /dev/null +++ b/Datadog/IntegrationUnitTests/CrashReporting/GeneratingBacktraceTests.swift @@ -0,0 +1,104 @@ +/* + * 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 DatadogCrashReporting +@testable import DatadogInternal + +/// Tests integration of `DatadogCore` and `DatadogCrashReporting` for backtrace generation. +class GeneratingBacktraceTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy(context: .mockWith(trackingConsent: .granted)) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + super.tearDown() + } + + func testGeneratingBacktraceOfTheCurrentThread() throws { + // Given + CrashReporting.enable(in: core) + XCTAssertNotNil(core.get(feature: BacktraceReportingFeature.self), "`BacktraceReportingFeature` must be registered") + + // When + let backtrace = try XCTUnwrap(core.backtraceReporter.generateBacktrace()) + + // Then + XCTAssertGreaterThan(backtrace.threads.count, 0, "Some thread(s) should be recorded") + XCTAssertGreaterThan(backtrace.binaryImages.count, 0, "Some binary image(s) should be recorded") + XCTAssertFalse(backtrace.threads.contains(where: { $0.crashed }), "No thread should be marked as crashed") + + XCTAssertTrue( + backtrace.stack.contains("DatadogCoreTests"), + "Backtrace stack should include at least one frame from `DatadogCoreTests` image" + ) + XCTAssertTrue( + backtrace.stack.contains("XCTest"), + "Backtrace stack should include at least one frame from `XCTest` image" + ) + #if os(iOS) + XCTAssertTrue( + backtrace.binaryImages.contains(where: { $0.libraryName == "DatadogCoreTests iOS" }), + "Backtrace should include the image for `DatadogCoreTests iOS`" + ) + #elseif os(tvOS) + XCTAssertTrue( + backtrace.binaryImages.contains(where: { $0.libraryName == "DatadogCoreTests tvOS" }), + "Backtrace should include the image for `DatadogCoreTests tvOS`" + ) + #endif + XCTAssertTrue( + // Assert on prefix as it is `XCTestCore` on iOS 15+ and `XCTest` earlier: + backtrace.binaryImages.contains(where: { $0.libraryName.hasPrefix("XCTest") }), + "Backtrace should include the image for `XCTest`" + ) + } + + func testGeneratingBacktraceOfTheMainThread() throws { + // Given + CrashReporting.enable(in: core) + + // When + XCTAssertTrue(Thread.current.isMainThread) + let threadID = Thread.currentThreadID + let backtrace = try XCTUnwrap(core.backtraceReporter.generateBacktrace(threadID: threadID)) + + // Then + XCTAssertFalse(backtrace.stack.isEmpty) + XCTAssertTrue(backtrace.stack.contains(uiKitLibraryName), "Main thread stack should include UIKit symbols") + } + + func testGeneratingBacktraceOfSecondaryThread() throws { + // Given + CrashReporting.enable(in: core) + + // When + let semaphore = DispatchSemaphore(value: 0) + var threadID: ThreadID? + + let thread = Thread { + XCTAssertFalse(Thread.current.isMainThread) + threadID = Thread.currentThreadID + semaphore.signal() + } + + thread.start() + XCTAssertEqual(semaphore.wait(timeout: .now() + 5), .success) + thread.cancel() + + let backtrace = try XCTUnwrap(core.backtraceReporter.generateBacktrace(threadID: threadID!)) + + // Then + XCTAssertFalse(backtrace.stack.isEmpty) + XCTAssertFalse(backtrace.stack.contains(uiKitLibraryName), "Secondary thread stack should NOT include UIKit symbols") + } +} diff --git a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift new file mode 100644 index 0000000000..e1789398c6 --- /dev/null +++ b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift @@ -0,0 +1,128 @@ +/* + * 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 DatadogCrashReporting +import DatadogInternal +@testable import DatadogLogs +@testable import DatadogRUM + +/// A crash reporter mock with two capabilities: +/// - notifying a pending crash report found at SDK init, +/// - recording crash context data injected from SDK core and features like RUM. +private class CrashReporterMock: CrashReportingPlugin { + @ReadWriteLock + var pendingCrashReport: DDCrashReport? + @ReadWriteLock + var injectedContext: Data? = nil + /// Custom backtrace reporter injected to the plugin. + var injectedBacktraceReporter: BacktraceReporting? + + init(pendingCrashReport: DDCrashReport? = nil) { + self.pendingCrashReport = pendingCrashReport + } + + func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) { _ = completion(pendingCrashReport) } + func inject(context: Data) { injectedContext = context } + var backtraceReporter: BacktraceReporting? { injectedBacktraceReporter } +} + +/// Covers broad scenarios of sending Crash Reports. +class SendingCrashReportTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy(context: .mockWith(trackingConsent: .granted)) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + super.tearDown() + } + + func testWhenSDKStartsWithPendingCrashReport_itSendsItAsLogAndRUMEvent() throws { + // Given + let crashContext: CrashContext = .mockWith( + trackingConsent: .granted, // CR from the app session that has enabled data collection + lastIsAppInForeground: true, // CR occurred while the app was in the foreground + lastRUMAttributes: GlobalRUMAttributes(attributes: mockRandomAttributes()), + 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) + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + CrashReporting.enable(with: CrashReporterMock(pendingCrashReport: crashReport), in: core) + + // Then (an emergency log is sent) + let log = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self).first) + XCTAssertEqual(log.status, .emergency) + XCTAssertEqual(log.message, crashReport.message) + XCTAssertEqual(log.error?.message, crashReport.message) + XCTAssertEqual(log.error?.kind, crashReport.type) + XCTAssertEqual(log.error?.stack, crashReport.stack) + 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]) + XCTAssertNotNil(log.attributes.internalAttributes?[DDError.wasTruncated]) + + // Then (RUMError is sent) + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self).first) + XCTAssertEqual(rumEvent.error.message, crashReport.message) + XCTAssertEqual(rumEvent.error.type, crashReport.type) + XCTAssertEqual(rumEvent.error.stack, crashReport.stack) + XCTAssertNotNil(rumEvent.error.threads) + XCTAssertNotNil(rumEvent.error.binaryImages) + XCTAssertNotNil(rumEvent.error.meta) + XCTAssertNotNil(rumEvent.error.wasTruncated) + let contextAttributes = try XCTUnwrap(rumEvent.context?.contextInfo) + let lastRUMAttributes = try XCTUnwrap(crashContext.lastRUMAttributes?.attributes) + DDAssertJSONEqual(contextAttributes, lastRUMAttributes.merging(crashReportAttributes) { $1 }) + } + + func testWhenSendingCrashReportAsLog_itIsLinkedToTheRUMSessionThatHasCrashed() throws { + let crashReporter = CrashReporterMock() + + // Given (RUM session) + Logs.enable(with: .init(), in: core) + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + CrashReporting.enable(with: crashReporter, in: core) + RUMMonitor.shared(in: core).startView(key: "view-1", name: "FirstView") + + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last) + + // Flush async tasks in Crash Reporting feature (this is yet not a part of `core.flushAndTearDown()` today) + // TODO: RUM-2766 Stop core instance with completion + (core.get(feature: CrashReportingFeature.self)!.crashContextProvider as! CrashContextCoreProvider).flush() + core.flushAndTearDown() + + // When (starting an SDK with pending crash report) + core = DatadogCoreProxy() + + let crashReport: DDCrashReport = .mockRandomWith( // mock a CR with context injected from previous instance of the SDK + contextData: crashReporter.injectedContext! + ) + + Logs.enable(with: .init(), in: core) + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + CrashReporting.enable(with: CrashReporterMock(pendingCrashReport: crashReport), in: core) + + // Then (an emergency log is sent) + let log = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self).first) + XCTAssertEqual(log.status, .emergency) + XCTAssertEqual(log.message, crashReport.message) + XCTAssertEqual(log.attributes.internalAttributes?["application_id"] as? String, rumEvent.application.id) + XCTAssertEqual(log.attributes.internalAttributes?["session_id"] as? String, rumEvent.session.id) + XCTAssertEqual(log.attributes.internalAttributes?["view.id"] as? String, rumEvent.view.id) + } +} diff --git a/Datadog/IntegrationUnitTests/Internal/CoreMetricsIntegrationTests.swift b/Datadog/IntegrationUnitTests/Internal/CoreMetricsIntegrationTests.swift new file mode 100644 index 0000000000..7effd6da6c --- /dev/null +++ b/Datadog/IntegrationUnitTests/Internal/CoreMetricsIntegrationTests.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 XCTest +@testable import DatadogCore +@testable import DatadogRUM +@testable import DatadogLogs +@testable import DatadogTrace +#if !os(tvOS) +@testable import DatadogSessionReplay +#endif + +class CoreMetricsIntegrationTests: XCTestCase { + func testResolvingTrackValueFromFeatureName() { + XCTAssertEqual(BatchMetric.trackValue(for: RUMFeature.name), "rum") + XCTAssertEqual(BatchMetric.trackValue(for: TraceFeature.name), "trace") + XCTAssertEqual(BatchMetric.trackValue(for: LogsFeature.name), "logs") + #if !os(tvOS) + XCTAssertEqual(BatchMetric.trackValue(for: SessionReplayFeature.name), "sr") + XCTAssertEqual(BatchMetric.trackValue(for: ResourcesFeature.name), "sr-resources") + #endif + } +} diff --git a/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift new file mode 100644 index 0000000000..8971d5900c --- /dev/null +++ b/Datadog/IntegrationUnitTests/Public/CoreTelemetryIntegrationTests.swift @@ -0,0 +1,261 @@ +/* + * 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 DatadogCore +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +class CoreTelemetryIntegrationTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy() + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + } + + func testGivenRUMEnabled_telemetryEventsAreSent() throws { + // Given + var config = RUM.Configuration(applicationID: .mockAny()) + config.telemetrySampleRate = .maxSampleRate + RUM.enable(with: config, in: core) + + // When + core.telemetry.debug("Debug Telemetry", attributes: ["debug.attribute": 42]) + #sourceLocation(file: "File.swift", line: 42) + core.telemetry.error("Error Telemetry") + #sourceLocation() + core.telemetry.metric(name: "Metric Name", attributes: ["metric.attribute": 42], sampleRate: 100) + core.telemetry.stopMethodCalled( + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 + ) + + // Then + let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryErrorEvent.self) + + XCTAssertEqual(debugEvents.count, 3) // metrics are transported as debug events + XCTAssertEqual(errorEvents.count, 1) + + let debug = debugEvents[0] + XCTAssertEqual(debug.telemetry.message, "Debug Telemetry") + DDAssertReflectionEqual(debug.telemetry.telemetryInfo, ["debug.attribute": 42]) + + let error = errorEvents[0] + XCTAssertEqual(error.telemetry.message, "Error Telemetry") + XCTAssertEqual(error.telemetry.error?.kind, "\(moduleName())/File.swift") + XCTAssertEqual(error.telemetry.error?.stack, "\(moduleName())/File.swift:42") + + 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) + + let methodCalledMetric = debugEvents[2] + XCTAssertEqual(methodCalledMetric.telemetry.message, "[Mobile Metric] Method Called") + } + + func testGivenRUMEnabled_whenNoViewIsActive_telemetryEventsAreLinkedToSession() throws { + // Given + var config = RUM.Configuration(applicationID: "rum-app-id") + config.telemetrySampleRate = .maxSampleRate + RUM.enable(with: config, in: core) + + // When + RUMMonitor.shared(in: core).startView(key: "View") + RUMMonitor.shared(in: core).stopView(key: "View") + + // Then + core.telemetry.debug("Debug Telemetry") + core.telemetry.error("Error Telemetry") + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) + core.telemetry.stopMethodCalled( + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 + ) + + let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryErrorEvent.self) + + let debug = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "Debug Telemetry" })) + XCTAssertEqual(debug.application?.id, "rum-app-id") + XCTAssertNotNil(debug.session?.id) + XCTAssertNil(debug.view?.id) + XCTAssertNil(debug.action?.id) + + let error = try XCTUnwrap(errorEvents.first) + XCTAssertEqual(error.application?.id, "rum-app-id") + XCTAssertNotNil(error.session?.id) + XCTAssertNil(error.view?.id) + XCTAssertNil(error.action?.id) + + let metric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Metric Name" })) + XCTAssertEqual(metric.application?.id, "rum-app-id") + XCTAssertNotNil(metric.session?.id) + XCTAssertNil(metric.view?.id) + XCTAssertNil(metric.action?.id) + + let methodCalledMetric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Method Called" })) + XCTAssertEqual(methodCalledMetric.application?.id, "rum-app-id") + XCTAssertNotNil(methodCalledMetric.session?.id) + XCTAssertNil(methodCalledMetric.view?.id) + XCTAssertNil(methodCalledMetric.action?.id) + } + + func testGivenRUMEnabled_whenViewIsActive_telemetryEventsAreLinkedToView() throws { + // Given + var config = RUM.Configuration(applicationID: "rum-app-id") + config.telemetrySampleRate = .maxSampleRate + RUM.enable(with: config, in: core) + + // When + RUMMonitor.shared(in: core).startView(key: .mockRandom()) + + // Then + core.telemetry.debug("Debug Telemetry") + core.telemetry.error("Error Telemetry") + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) + core.telemetry.stopMethodCalled( + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 + ) + + let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryErrorEvent.self) + + let debug = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "Debug Telemetry" })) + XCTAssertEqual(debug.application?.id, "rum-app-id") + XCTAssertNotNil(debug.session?.id) + XCTAssertNotNil(debug.view?.id) + XCTAssertNil(debug.action?.id) + + let error = try XCTUnwrap(errorEvents.first) + XCTAssertEqual(error.application?.id, "rum-app-id") + XCTAssertNotNil(error.session?.id) + XCTAssertNotNil(error.view?.id) + XCTAssertNil(error.action?.id) + + let metric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Metric Name" })) + XCTAssertEqual(metric.application?.id, "rum-app-id") + XCTAssertNotNil(metric.session?.id) + XCTAssertNotNil(metric.view?.id) + XCTAssertNil(metric.action?.id) + + let methodCalledMetric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Method Called" })) + XCTAssertEqual(methodCalledMetric.application?.id, "rum-app-id") + XCTAssertNotNil(methodCalledMetric.session?.id) + XCTAssertNotNil(methodCalledMetric.view?.id) + XCTAssertNil(methodCalledMetric.action?.id) + } + + func testGivenRUMEnabled_whenActionIsActive_telemetryEventsAreLinkedToAction() throws { + // Given + var config = RUM.Configuration(applicationID: "rum-app-id") + config.telemetrySampleRate = .maxSampleRate + RUM.enable(with: config, in: core) + + // When + RUMMonitor.shared(in: core).startView(key: .mockRandom()) + RUMMonitor.shared(in: core).addAction(type: .tap, name: "tap") + + // Then + core.telemetry.debug("Debug Telemetry") + core.telemetry.error("Error Telemetry") + core.telemetry.metric(name: "Metric Name", attributes: [:], sampleRate: 100) + core.telemetry.stopMethodCalled( + core.telemetry.startMethodCalled(operationName: .mockRandom(), callerClass: .mockRandom(), headSampleRate: 100), + tailSampleRate: 100 + ) + + let debugEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryErrorEvent.self) + + let debug = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "Debug Telemetry" })) + XCTAssertEqual(debug.application?.id, "rum-app-id") + XCTAssertNotNil(debug.session?.id) + XCTAssertNotNil(debug.view?.id) + XCTAssertNotNil(debug.action?.id) + + let error = try XCTUnwrap(errorEvents.first) + XCTAssertEqual(error.application?.id, "rum-app-id") + XCTAssertNotNil(error.session?.id) + XCTAssertNotNil(error.view?.id) + XCTAssertNotNil(error.action?.id) + + let metric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Metric Name" })) + XCTAssertEqual(metric.application?.id, "rum-app-id") + XCTAssertNotNil(metric.session?.id) + XCTAssertNotNil(metric.view?.id) + XCTAssertNotNil(metric.action?.id) + + let methodCalledMetric = try XCTUnwrap(debugEvents.first(where: { $0.telemetry.message == "[Mobile Metric] Method Called" })) + XCTAssertEqual(methodCalledMetric.application?.id, "rum-app-id") + XCTAssertNotNil(methodCalledMetric.session?.id) + 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/Datadog+MultipleInstancesIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/Datadog+MultipleInstancesIntegrationTests.swift new file mode 100644 index 0000000000..181e8f172e --- /dev/null +++ b/Datadog/IntegrationUnitTests/Public/Datadog+MultipleInstancesIntegrationTests.swift @@ -0,0 +1,100 @@ +/* + * 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 DatadogCore +import DatadogInternal +import DatadogLogs + +class Datadog_MultipleInstancesIntegrationTests: XCTestCase { + /// The configuraiton of default instance of SDK. + private var defaultInstanceConfig = Datadog.Configuration(clientToken: "main-token", env: "default-env") + /// The configuraiton of custom instance of SDK. + private var customInstanceConfig = Datadog.Configuration(clientToken: "custom-token", env: "custom-env") + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + + // Root system directory for both instances: + let systemDirectory = Directory(url: temporaryDirectory) + defaultInstanceConfig.systemDirectory = { systemDirectory } + customInstanceConfig.systemDirectory = { systemDirectory } + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + func testGivenTwoInstancesOfSDK_whenCollectingLogs_thenEachSDKUploadsItsOwnData() throws { + let customInstanceName = "custom" + let numberOfLogs = 10 + let defaultHTTPClient = HTTPClientMock(responseCode: 200) + let customHTTPClient = HTTPClientMock(responseCode: 200) + defaultInstanceConfig.httpClientFactory = { _ in defaultHTTPClient } + customInstanceConfig.httpClientFactory = { _ in customHTTPClient } + defaultInstanceConfig.bundle = .mockWith(bundleIdentifier: "com.bundle.default", CFBundleShortVersionString: "1.0-default") + customInstanceConfig.bundle = .mockWith(bundleIdentifier: "com.bundle.custom", CFBundleShortVersionString: "1.0-custom") + + // Given + Datadog.initialize(with: defaultInstanceConfig, trackingConsent: .granted) + Datadog.initialize(with: customInstanceConfig, trackingConsent: .granted, instanceName: customInstanceName) + + Logs.enable(with: .init()) + Logs.enable(with: .init(), in: Datadog.sdkInstance(named: customInstanceName)) + + let defaultLogger = Logger.create() + let customLogger = Logger.create(in: Datadog.sdkInstance(named: customInstanceName)) + + // When + for _ in 0.. 0) + } + + func testResourceAttributesProvider_givenURLSessionDataTaskRequestWithCompletionHandler() { + core = DatadogCoreProxy( + context: .mockWith( + env: "test", + version: "1.1.1", + serverTimeOffset: 123 + ) + ) + + let providerExpectation = expectation(description: "provider called") + var providerInfo: (resp: URLResponse?, data: Data?, err: Error?)? + + RUM.enable( + with: .init( + applicationID: .mockAny(), + urlSessionTracking: .init( + resourceAttributesProvider: { _, resp, data, err in + providerInfo = (resp, data, err) + providerExpectation.fulfill() + return [:] + } + ) + ), + in: core + ) + + URLSessionInstrumentation.enable( + with: .init( + delegateClass: InstrumentedSessionDelegate.self + ), + in: core + ) + + let session = URLSession( + configuration: .ephemeral, + delegate: InstrumentedSessionDelegate(), + delegateQueue: nil + ) + let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) + + let taskExpectation = self.expectation(description: "task completed") + 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(providerInfo?.resp, taskInfo?.resp) + XCTAssertEqual(providerInfo?.data, taskInfo?.data) + XCTAssertEqual(providerInfo?.err as? NSError, taskInfo?.err as? NSError) + } + + class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + } + } +} diff --git a/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift new file mode 100644 index 0000000000..568bd0cd29 --- /dev/null +++ b/Datadog/IntegrationUnitTests/Public/WebLogIntegrationTests.swift @@ -0,0 +1,144 @@ +/* + * 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 + +#if !os(tvOS) + +import DatadogInternal + +@testable import DatadogLogs +@testable import DatadogRUM +@testable import DatadogWebViewTracking + +class WebLogIntegrationTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var controller: WKUserContentControllerMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy( + context: .mockWith( + env: "test", + version: "1.1.1", + serverTimeOffset: 123 + ) + ) + + controller = WKUserContentControllerMock() + + WebViewTracking.enable( + tracking: controller, + hosts: [], + hostsSanitizer: HostsSanitizer(), + logsSampleRate: 100, + in: core + ) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + controller = nil + } + + func testWebLogIntegration() throws { + // Given + Logs.enable(in: core) + + let body = """ + { + "eventType": "log", + "event": { + "date" : \(1_635_932_927_012), + "status": "debug", + "message": "message", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "view": { + "referrer": "", + "url": "https://datadoghq.dev/browser-sdk-test-playground" + } + } + } + """ + + // When + controller.send(body: body) + controller.flush() + + // Then + let logMatcher = try XCTUnwrap(core.waitAndReturnLogMatchers().first) + try logMatcher.assertItFullyMatches( + jsonString: """ + { + "date": \(1_635_932_927_012 + 123.toInt64Milliseconds), + "ddtags": "version:1.1.1,env:test", + "message": "message", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "status": "debug", + "view": { + "referrer": "", + "url": "https://datadoghq.dev/browser-sdk-test-playground" + }, + } + """ + ) + } + + func testWebLogWithRUMIntegration() throws { + // Given + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + + Logs.enable(in: core) + RUM.enable(with: .mockWith(applicationID: randomApplicationID) { + $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) + }, in: core) + + let body = """ + { + "eventType": "log", + "event": { + "date" : \(1_635_932_927_012), + "status": "debug", + "message": "message", + "session_id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "view": { + "referrer": "", + "url": "https://datadoghq.dev/browser-sdk-test-playground" + } + } + } + """ + + // When + RUMMonitor.shared(in: core).startView(key: "web-view") + controller.send(body: body) + controller.flush() + + // Then + let expectedUUID = randomUUID.uuidString.lowercased() + let logMatcher = try XCTUnwrap(core.waitAndReturnLogMatchers().first) + try logMatcher.assertItFullyMatches( + jsonString: """ + { + "date": \(1_635_932_927_012 + 123.toInt64Milliseconds), + "ddtags": "version:1.1.1,env:test", + "message": "message", + "application_id": "\(randomApplicationID)", + "session_id": "\(expectedUUID)", + "view.id": "\(expectedUUID)", + "status": "debug", + "view": { + "referrer": "", + "url": "https://datadoghq.dev/browser-sdk-test-playground" + }, + } + """ + ) + } +} + +#endif diff --git a/Datadog/IntegrationUnitTests/RUM/AppHangsMonitoringTests.swift b/Datadog/IntegrationUnitTests/RUM/AppHangsMonitoringTests.swift new file mode 100644 index 0000000000..f026948ca6 --- /dev/null +++ b/Datadog/IntegrationUnitTests/RUM/AppHangsMonitoringTests.swift @@ -0,0 +1,147 @@ +/* + * 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 + +/// Test case covering scenarios of App Hangs monitoring in RUM. +class AppHangsMonitoringTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var rumConfig = RUM.Configuration(applicationID: .mockAny()) + private var hangDuration: TimeInterval! // swiftlint:disable:this implicitly_unwrapped_optional + /// Use main queue mock, otherwise any `waitForExpectations(timeout:)` would be considered an app hang and may cause dead locks. + private let mainQueue = DispatchQueue(label: "main-queue-mock", qos: .userInteractive) + + private var expectedHangDurationRangeNs: ClosedRange { + let min = hangDuration.toInt64Nanoseconds / 2 // -50% margin + let max = hangDuration.toInt64Nanoseconds * 5 // +500% margin to avoid flakiness + return (min...max) + } + + override func setUp() { + rumConfig.mainQueue = mainQueue + rumConfig.appHangThreshold = 0.4 + hangDuration = rumConfig.appHangThreshold! * 1.25 + core = DatadogCoreProxy() + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + } + + func testWhenMainThreadIsHangedInitially_itTracksAppHangError() throws { + // Given + mainQueue.sync { + RUM.enable(with: rumConfig, in: core) + + // When + Thread.sleep(forTimeInterval: hangDuration) // hang right after SDK is initialized + } + + // Then + try flushHangsMonitoring() + let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let appHangError = try XCTUnwrap(errors.first) + let actualHangDuration = try XCTUnwrap(appHangError.freeze?.duration) + + XCTAssertEqual(appHangError.error.message, AppHangsMonitor.Constants.appHangErrorMessage) + XCTAssertEqual(appHangError.error.type, AppHangsMonitor.Constants.appHangErrorType) + XCTAssertEqual(appHangError.error.category, .appHang) + XCTAssertTrue(expectedHangDurationRangeNs.contains(actualHangDuration)) + } + + func testWhenMainThreadIsHangedAfterInit_itTracksAppHangError() throws { + // Given + mainQueue.sync { + RUM.enable(with: rumConfig, in: core) + } + + // When + mainQueue.sync { // hang in the main thread task that follows SDK is initialization + Thread.sleep(forTimeInterval: hangDuration) + } + + // Then + try flushHangsMonitoring() + let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let appHangError = try XCTUnwrap(errors.first) + let actualHangDuration = try XCTUnwrap(appHangError.freeze?.duration) + + XCTAssertEqual(appHangError.error.message, AppHangsMonitor.Constants.appHangErrorMessage) + XCTAssertEqual(appHangError.error.type, AppHangsMonitor.Constants.appHangErrorType) + XCTAssertEqual(appHangError.error.category, .appHang) + XCTAssertTrue(expectedHangDurationRangeNs.contains(actualHangDuration)) + } + + func testGivenRUMAndCrashReportingEnabled_whenMainThreadHangs_thenAppHangErrorIncludesStackTrace() throws { + // Given (initialize SDK on the main thread) + 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) + }, + ]) + + // When + mainQueue.sync { + Thread.sleep(forTimeInterval: hangDuration) + } + + // Then + try flushHangsMonitoring() + let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let appHangError = try XCTUnwrap(errors.first) + let mainThreadStack = try XCTUnwrap(appHangError.error.stack) + + XCTAssertEqual(appHangError.error.message, AppHangsMonitor.Constants.appHangErrorMessage) + XCTAssertEqual(appHangError.error.type, AppHangsMonitor.Constants.appHangErrorType) + XCTAssertTrue(mainThreadStack.contains(uiKitLibraryName), "Main thread stack should include UIKit symbols") + XCTAssertEqual(appHangError.error.source, .source) + XCTAssertNotNil(appHangError.error.threads, "Other threads should be available") + XCTAssertNotNil(appHangError.error.binaryImages, "Binary Images should be available for symbolication") + } + + func testGivenOnlyRUMEnabled_whenMainThreadHangs_itTracksAppHangWithNoStackTrace() throws { + // Given + mainQueue.sync { + RUM.enable(with: rumConfig, in: core) + } + + // When + mainQueue.sync { + Thread.sleep(forTimeInterval: hangDuration) + } + + // Then + try flushHangsMonitoring() + let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let appHangError = try XCTUnwrap(errors.first) + + XCTAssertEqual(appHangError.error.message, AppHangsMonitor.Constants.appHangErrorMessage) + XCTAssertEqual(appHangError.error.type, AppHangsMonitor.Constants.appHangErrorType) + XCTAssertEqual(appHangError.error.stack, AppHangsMonitor.Constants.appHangStackNotAvailableErrorMessage) + XCTAssertEqual(appHangError.error.source, .source) + XCTAssertNil(appHangError.error.threads, "Threads should be unavailable as CrashReporting was not enabled") + XCTAssertNil(appHangError.error.binaryImages, "Binary Images should be unavailable as CrashReporting was not enabled") + } + + private func flushHangsMonitoring() throws { + mainQueue.sync {} // flush the mock main queue (by awaiting the next task after the hang) + + // Flush the watchdog thread (by awaiting on the real main thread), to make sure the thread is done with any hang processing + // and it is idle: + let hangObserver = try XCTUnwrap(core.get(feature: RUMFeature.self)?.instrumentation.appHangs) + hangObserver.flush() + } +} diff --git a/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift new file mode 100644 index 0000000000..fa5916ec01 --- /dev/null +++ b/Datadog/IntegrationUnitTests/RUM/SDKMetrics/RUMSessionEndedMetricIntegrationTests.swift @@ -0,0 +1,286 @@ +/* + * 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 DatadogRUM +@testable import DatadogInternal + +class RUMSessionEndedMetricIntegrationTests: XCTestCase { + private let dateProvider = DateProviderMock() + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var rumConfig: RUM.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy() + core.context = .mockWith( + launchTime: .mockWith(launchDate: dateProvider.now), + applicationStateHistory: .mockAppInForeground(since: dateProvider.now) + ) + rumConfig = RUM.Configuration(applicationID: .mockAny()) + rumConfig.telemetrySampleRate = .maxSampleRate + rumConfig.dateProvider = dateProvider + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + rumConfig = nil + } + + // MARK: - Conditions For Sending The Metric + + func testWhenSessionEndsWithStopAPI() throws { + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + monitor.startView(key: "key", name: "View") + + // When + monitor.stopSession() + + // Then + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + XCTAssertTrue(metricAttributes.wasStopped) + } + + func testWhenSessionEndsDueToInactivityTimeout() throws { + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + monitor.startView(key: "key1", name: "View1") + + // When + dateProvider.now += RUMSessionScope.Constants.sessionTimeoutDuration + 1.seconds + monitor.startView(key: "key2", name: "View2") + + // Then + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + XCTAssertFalse(metricAttributes.wasStopped) + } + + func testWhenSessionReachesMaxDuration() throws { + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + monitor.startView(key: "key", name: "View") + + // When + let deadline = dateProvider.now + RUMSessionScope.Constants.sessionMaxDuration * 1.5 + while dateProvider.now < deadline { + monitor.addAction(type: .custom, name: "action") + dateProvider.now += RUMSessionScope.Constants.sessionTimeoutDuration - 1.seconds + } + + // Then + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + XCTAssertFalse(metricAttributes.wasStopped) + } + + func testWhenSessionIsNotSampled_thenMetricIsNotSent() throws { + rumConfig.sessionSampleRate = 0 + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + monitor.startView(key: "key", name: "View") + + // When + monitor.stopSession() + + // Then + let events = core.waitAndReturnEventsData(ofFeature: RUMFeature.name, timeout: .now() + 0.5) + XCTAssertTrue(events.isEmpty) + } + + // MARK: - Reporting Session Attributes + + func testReportingSessionInformation() throws { + var currentSessionID: String? + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + monitor.startView(key: "key", name: "View") + monitor.currentSessionID { currentSessionID = $0 } + monitor.stopView(key: "key") + + // When + monitor.stopSession() + + // Then + 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 { + let startTime = dateProvider.now + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + dateProvider.now += 5.seconds + monitor.startView(key: "key1", name: "View1") + dateProvider.now += 5.seconds + monitor.startView(key: "key2", name: "View2") + dateProvider.now += 5.seconds + monitor.startView(key: "key3", name: "View3") + dateProvider.now += 5.seconds + monitor.stopView(key: "key3") + + // When + monitor.stopSession() + + // Then + let expectedDuration = dateProvider.now.timeIntervalSince(startTime) + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + XCTAssertEqual(metricAttributes.duration, expectedDuration.toInt64Nanoseconds) + } + + func testTrackingViewsCount() throws { + rumConfig.trackBackgroundEvents = true // enable tracking "Background" view + RUM.enable(with: rumConfig, in: core) + + // Given + let monitor = self.rumMonitor() + (0..<3).forEach { _ in + // Simulate app in foreground: + core.context = .mockWith(applicationStateHistory: .mockAppInForeground(since: dateProvider.now)) + + // Track 2 distinct views: + dateProvider.now += 5.seconds + monitor.startView(key: "key1", name: "View1") + dateProvider.now += 5.seconds + monitor.startView(key: "key2", name: "View2") + dateProvider.now += 5.seconds + monitor.stopView(key: "key2") + + // Simulate app in background: + core.context = .mockWith(applicationStateHistory: .mockAppInBackground(since: dateProvider.now)) + + // Track resource without view: + dateProvider.now += 1.seconds + monitor.startResource(resourceKey: "resource", url: .mockAny()) + dateProvider.now += 1.seconds + monitor.stopResource(resourceKey: "resource", response: .mockAny()) + } + + // When + monitor.stopSession() + + // Then + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + 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 = self.rumMonitor() + monitor.startView(key: "key", name: "View") + + core.flush() + (0..<9).forEach { _ in core.telemetry.error(id: "id1", message: .mockAny(), kind: "kind1", stack: .mockAny()) } + (0..<8).forEach { _ in core.telemetry.error(id: "id2", message: .mockAny(), kind: "kind2", stack: .mockAny()) } + (0..<7).forEach { _ in core.telemetry.error(id: "id3", message: .mockAny(), kind: "kind3", stack: .mockAny()) } + (0..<6).forEach { _ in core.telemetry.error(id: "id4", message: .mockAny(), kind: "kind4", stack: .mockAny()) } + (0..<5).forEach { _ in core.telemetry.error(id: "id5", message: .mockAny(), kind: "kind5", stack: .mockAny()) } + (0..<4).forEach { _ in core.telemetry.error(id: "id6", message: .mockAny(), kind: "kind6", stack: .mockAny()) } + core.flush() + + // When + monitor.stopSession() + + // Then + let metricAttributes = try XCTUnwrap(core.waitAndReturnSessionEndedMetricEvent()?.attributes) + XCTAssertEqual(metricAttributes.sdkErrorsCount.total, 39, "It should count all SDK errors") + XCTAssertEqual( + metricAttributes.sdkErrorsCount.byKind, + ["kind1": 9, "kind2": 8, "kind3": 7, "kind4": 6, "kind5": 5], + "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 + +private extension DatadogCoreProxy { + func waitAndReturnSessionEndedMetricEvent() -> TelemetryDebugEvent? { + let events = waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: TelemetryDebugEvent.self) + return events.first(where: { $0.telemetry.message == "[Mobile Metric] \(SessionEndedMetric.Constants.name)" }) + } +} + +private extension TelemetryDebugEvent { + var attributes: SessionEndedMetric.Attributes? { + return telemetry.telemetryInfo[SessionEndedMetric.Constants.rseKey] as? SessionEndedMetric.Attributes + } +} diff --git a/Datadog/IntegrationUnitTests/RUM/StartingRUMSessionTests.swift b/Datadog/IntegrationUnitTests/RUM/StartingRUMSessionTests.swift new file mode 100644 index 0000000000..9eeb75343d --- /dev/null +++ b/Datadog/IntegrationUnitTests/RUM/StartingRUMSessionTests.swift @@ -0,0 +1,332 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogRUM +import DatadogInternal +import TestUtilities + +/// Test case covering scenarios of starting RUM session. +class StartingRUMSessionTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var rumConfig: RUM.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + /// The date of app process start. + private let processStartTime = Date() + /// The time it took for app process to start. + private let processStartDuration: TimeInterval = 2.5 + /// The date of SDK init. + private lazy var sdkInitTime = processStartTime.addingTimeInterval(processStartDuration) + /// The date of first activity in RUM. + private lazy var firstRUMTime = sdkInitTime.addingTimeInterval(3) + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + rumConfig = RUM.Configuration(applicationID: .mockAny()) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + rumConfig = nil + super.tearDown() + } + + func testGivenAppLaunchInForegroundAndNoPrewarming_whenRUMisEnabled_itStartsAppLaunchViewAndSendsAppStartAction() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: false + ), + applicationStateHistory: .mockAppInForeground( + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 1) + XCTAssertTrue(try session.has(sessionPrecondition: .userAppLaunch), "Session must be marked as 'user app launch'") + + let initView = try XCTUnwrap(session.views.first) + XCTAssertTrue(initView.isApplicationLaunchView(), "Session should begin with 'app launch' view") + + let initViewEvent = try XCTUnwrap(initView.viewEvents.last) + XCTAssertEqual(initViewEvent.date, processStartTime.timeIntervalSince1970.toInt64Milliseconds, "The 'app launch' view should start at process launch") + XCTAssertEqual(initViewEvent.view.timeSpent, (sdkInitTime.timeIntervalSince(processStartTime)).toInt64Nanoseconds, "The 'app launch' view should span from app launch to 'sdk init'") + + let actionEvent = try XCTUnwrap(initView.actionEvents.first) + XCTAssertEqual(actionEvent.action.type, .applicationStart, "The 'app launch' view should send 'app start' action") + XCTAssertEqual(actionEvent.date, initViewEvent.date, "The 'app start' action must occur at the beginning of 'app launch' view") + XCTAssertEqual(actionEvent.action.loadingTime, processStartDuration.toInt64Nanoseconds, "The duration of 'app start' action must equal to the duration of process launch") + } + + func testGivenAppLaunchInForegroundAndNoPrewarming_whenRUMisEnabledAndViewIsTracked_itStartsWithAppLaunchViewAndSendsAppStartAction() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: false + ), + applicationStateHistory: .mockAppInForeground( + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).startView(key: "key", name: "FirstView") + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 2) + XCTAssertTrue(try session.has(sessionPrecondition: .userAppLaunch), "Session must be marked as 'user app launch'") + + let initView = try XCTUnwrap(session.views.first) + XCTAssertTrue(initView.isApplicationLaunchView(), "Session should begin with 'app launch' view") + + let initViewEvent = try XCTUnwrap(initView.viewEvents.last) + XCTAssertEqual(initViewEvent.date, processStartTime.timeIntervalSince1970.toInt64Milliseconds, "The 'app launch' view should start at process launch") + XCTAssertEqual(initViewEvent.view.timeSpent, (firstRUMTime.timeIntervalSince(processStartTime)).toInt64Nanoseconds, "The 'app launch' view should span from app launch to first view") + + let actionEvent = try XCTUnwrap(initView.actionEvents.first) + XCTAssertEqual(actionEvent.action.type, .applicationStart, "The 'app launch' view should send 'app start' action") + XCTAssertEqual(actionEvent.date, initViewEvent.date, "The 'app start' action must occur at the beginning of 'app launch' view") + XCTAssertEqual(actionEvent.action.loadingTime, processStartDuration.toInt64Nanoseconds, "The duration of 'app start' action must equal to the duration of process launch") + + let firstView = try XCTUnwrap(session.views.last) + XCTAssertEqual(firstView.name, "FirstView") + XCTAssertEqual(firstView.viewEvents.last?.date, firstRUMTime.timeIntervalSince1970.toInt64Milliseconds) + } + + func testGivenAppLaunchInBackgroundAndNoPrewarming_whenRUMisEnabledAndViewIsTracked_itStartsWithTrackedView() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: false + ), + applicationStateHistory: .mockAppInBackground( + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).startView(key: "key", name: "FirstView") + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 1) + XCTAssertTrue(try session.has(sessionPrecondition: .backgroundLaunch), "Session must be marked as 'background launch'") + + let firstView = try XCTUnwrap(session.views.first) + XCTAssertFalse(firstView.isApplicationLaunchView(), "Session should not begin with 'app launch' view") + XCTAssertEqual(firstView.name, "FirstView") + XCTAssertEqual(firstView.viewEvents.last?.date, firstRUMTime.timeIntervalSince1970.toInt64Milliseconds) + XCTAssertTrue(firstView.actionEvents.isEmpty, "The 'app start' action should not be sent") + } + + func testGivenAppLaunchWithPrewarming_whenRUMisEnabledAndViewIsTrackedInBackground_itStartsWithTrackedView() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: true + ), + applicationStateHistory: .mockAppInBackground( // active prewarm implies background + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).startView(key: "key", name: "FirstView") + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 1) + XCTAssertTrue(try session.has(sessionPrecondition: .prewarm), "Session must be marked as 'prewarm'") + + let firstView = try XCTUnwrap(session.views.first) + XCTAssertFalse(firstView.isApplicationLaunchView(), "Session should not begin with 'app launch' view") + XCTAssertEqual(firstView.name, "FirstView") + XCTAssertEqual(firstView.viewEvents.last?.date, firstRUMTime.timeIntervalSince1970.toInt64Milliseconds) + XCTAssertTrue(firstView.actionEvents.isEmpty, "The 'app start' action should not be sent") + } + + func testGivenAppLaunchWithPrewarming_whenRUMisEnabledAndViewIsTrackedInForeground_itStartsWithAppLaunchViewAndSendsAppStartAction() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: true + ), + applicationStateHistory: AppStateHistory( + initialSnapshot: .init(state: .background, date: processStartTime), // active prewarm implies background + recentDate: firstRUMTime, + snapshots: [ + .init(state: .active, date: firstRUMTime.addingTimeInterval(-0.5)) // become active shortly before view is started + ] + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).startView(key: "key", name: "FirstView") + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 2) + XCTAssertTrue(try session.has(sessionPrecondition: .prewarm), "Session must be marked as 'prewarm'") + + let initView = try XCTUnwrap(session.views.first) + XCTAssertTrue(initView.isApplicationLaunchView(), "Session should begin with 'app launch' view") + + let initViewEvent = try XCTUnwrap(initView.viewEvents.last) + XCTAssertEqual(initViewEvent.date, sdkInitTime.timeIntervalSince1970.toInt64Milliseconds, "The 'app launch' view should start at sdk init") + XCTAssertEqual(initViewEvent.view.timeSpent, (firstRUMTime.timeIntervalSince(sdkInitTime)).toInt64Nanoseconds, "The 'app launch' view should span from sdk init to first view") + + let actionEvent = try XCTUnwrap(initView.actionEvents.first) + XCTAssertEqual(actionEvent.action.type, .applicationStart, "The 'app launch' view should send 'app start' action") + XCTAssertEqual(actionEvent.date, initViewEvent.date, "The 'app start' action must occur at the beginning of 'app launch' view") + XCTAssertNil(actionEvent.action.loadingTime, "The 'app start' action must have no 'loading time' set as we can't know it for prewarmed session") + + let firstView = try XCTUnwrap(session.views.last) + XCTAssertEqual(firstView.name, "FirstView") + XCTAssertEqual(firstView.viewEvents.last?.date, firstRUMTime.timeIntervalSince1970.toInt64Milliseconds) + } + + // MARK: - Test Background Events Tracking + + func testGivenAppLaunchWithPrewarmingAndBETEnabled_whenRUMisEnabledAndInteractionEventIsTracked_itStartsWithBackgroundView() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: true + ), + applicationStateHistory: .mockAppInBackground( // active prewarm implies background + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = true + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).addAction(type: .custom, name: "CustomAction") + + // Then + let session = try RUMSessionMatcher + .groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers()) + .takeSingle() + + XCTAssertEqual(session.views.count, 1) + XCTAssertTrue(try session.has(sessionPrecondition: .prewarm), "Session must be marked as 'prewarm'") + + let initView = try XCTUnwrap(session.views.first) + XCTAssertTrue(initView.isBackgroundView(), "Session should begin with 'background' view") + XCTAssertFalse(initView.actionEvents.contains(where: { $0.action.type == .applicationStart }), "The 'app start' action should not be sent") + + let actionEvent = try XCTUnwrap(initView.actionEvents.first) + XCTAssertEqual(actionEvent.date, firstRUMTime.timeIntervalSince1970.toInt64Milliseconds) + XCTAssertEqual(actionEvent.action.target?.name, "CustomAction") + } + + func testGivenAppLaunchWithPrewarmingAndBETDisabled_whenRUMisEnabledAndInteractionEventIsTracked_itIgnoresTheEvent() throws { + // Given + core.context = .mockWith( + sdkInitDate: sdkInitTime, + launchTime: .mockWith( + launchTime: processStartDuration, + launchDate: processStartTime, + isActivePrewarm: true + ), + applicationStateHistory: .mockAppInBackground( // active prewarm implies background + since: processStartTime + ) + ) + let rumTime = DateProviderMock() + rumConfig.dateProvider = rumTime + rumConfig.trackBackgroundEvents = false + + // When + rumTime.now = sdkInitTime + RUM.enable(with: rumConfig, in: core) + + rumTime.now = firstRUMTime + RUMMonitor.shared(in: core).addAction(type: .custom, name: "CustomAction") + + // Then + let events = core.waitAndReturnEventsData(ofFeature: RUMFeature.name, timeout: .now() + 1) + XCTAssertTrue(events.isEmpty, "The session should not be started (no events must be sent)") + } +} 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 new file mode 100644 index 0000000000..7b3eb0572d --- /dev/null +++ b/Datadog/IntegrationUnitTests/RUM/WebEventIntegrationTests.swift @@ -0,0 +1,281 @@ +/* + * 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 +#if !os(tvOS) + +import DatadogInternal +import TestUtilities + +@testable import DatadogRUM +@testable import DatadogWebViewTracking + +class WebEventIntegrationTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var controller: WKUserContentControllerMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy( + context: .mockWith( + env: "test", + version: "1.1.1", + serverTimeOffset: 123 + ) + ) + + controller = WKUserContentControllerMock() + + WebViewTracking.enable( + tracking: controller, + hosts: [], + hostsSanitizer: HostsSanitizer(), + logsSampleRate: 100, + in: core + ) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + controller = nil + } + + func testWebEventIntegration() throws { + // Given + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + + RUM.enable(with: .mockWith(applicationID: randomApplicationID) { + $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) + }, in: core) + + let body = """ + { + "eventType": "view", + "event": { + "application": { + "id": "xxx" + }, + "date": \(1_635_932_927_012), + "service": "super", + "session": { + "id": "0110cab4-7471-480e-aa4e-7ce039ced355", + "type": "user", + "has_replay": true + }, + "type": "view", + "view": { + "action": { + "count": 0 + }, + "cumulative_layout_shift": 0, + "dom_complete": 152800000, + "dom_content_loaded": 118300000, + "dom_interactive": 116400000, + "error": { + "count": 0 + }, + "first_contentful_paint": 121300000, + "id": "64308fd4-83f9-48cb-b3e1-1e91f6721230", + "in_foreground_periods": [], + "is_active": true, + "largest_contentful_paint": 121299000, + "load_event": 152800000, + "loading_time": 152800000, + "loading_type": "initial_load", + "long_task": { + "count": 0 + }, + "referrer": "", + "resource": { + "count": 3 + }, + "time_spent": 3120000000, + "url": "http://localhost:8080/test.html", + }, + "_dd": { + "document_version": 2, + "drift": 0, + "format_version": 2, + "replay_stats": { + "records_count": 10, + "segments_count": 1, + "segments_total_raw_size": 10 + } + } + }, + "tags": [ + "browser_sdk_version:3.6.13" + ] + } + """ + + // When + RUMMonitor.shared(in: core).startView(key: "web-view") + controller.send(body: body) + controller.flush() + + // Then + let expectedUUID = randomUUID.uuidString.lowercased() + let rumMatcher = try XCTUnwrap(core.waitAndReturnRUMEventMatchers().last) + try rumMatcher.assertItFullyMatches( + jsonString: """ + { + "application": { + "id": "\(randomApplicationID)" + }, + "date": \(1_635_932_927_012 + 123.toInt64Milliseconds), + "service": "super", + "session": { + "id": "\(expectedUUID)", + "type": "user" + }, + "type": "view", + "view": { + "action": { + "count": 0 + }, + "cumulative_layout_shift": 0, + "dom_complete": 152800000, + "dom_content_loaded": 118300000, + "dom_interactive": 116400000, + "error": { + "count": 0 + }, + "first_contentful_paint": 121300000, + "id": "64308fd4-83f9-48cb-b3e1-1e91f6721230", + "in_foreground_periods": [], + "is_active": true, + "largest_contentful_paint": 121299000, + "load_event": 152800000, + "loading_time": 152800000, + "loading_type": "initial_load", + "long_task": { + "count": 0 + }, + "referrer": "", + "resource": { + "count": 3 + }, + "time_spent": 3120000000, + "url": "http://localhost:8080/test.html" + }, + "_dd": { + "document_version": 2, + "drift": 0, + "format_version": 2 + } + } + """ + ) + } + + func testWebTelemetryIntegration() throws { + // Given + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + + RUM.enable(with: .mockWith(applicationID: randomApplicationID) { + $0.uuidGenerator = RUMUUIDGeneratorMock(uuid: randomUUID) + }, in: core) + + let body = """ + { + "eventType": "internal_telemetry", + "event": + { + "type": "telemetry", + "date": 1712069357432, + "service": "browser-rum-sdk", + "version": "5.2.0-b93ed472a4f14fbf2bcd1bc2c9faacb4abbeed82", + "source": "browser", + "_dd": { "format_version": 2 }, + "telemetry": + { + "type": "configuration", + "configuration": + { + "session_replay_sample_rate": 100, + "use_allowed_tracing_urls": false, + "selected_tracing_propagators": [], + "default_privacy_level": "allow", + "use_excluded_activity_urls": false, + "use_worker_url": false, + "track_user_interactions": true, + "track_resources": true, + "track_long_task": true, + "session_sample_rate": 100, + "telemetry_sample_rate": 100, + "use_before_send": false, + "use_proxy": false, + "allow_fallback_to_local_storage": false, + "store_contexts_across_pages": false, + "allow_untrusted_events": false + }, + "runtime_env": { "is_local_file": false, "is_worker": false } + }, + "experimental_features": [], + "application": { "id": "00000000-aaaa-0000-aaaa-000000000000" }, + "session": { "id": "00000000-aaaa-0000-aaaa-000000000000" }, + "view": {}, + "action": { "id": [] } + } + } + """ + + // When + RUMMonitor.shared(in: core).startView(key: "web-view") + controller.send(body: body) + controller.flush() + + // Then + let expectedUUID = randomUUID.uuidString.lowercased() + let rumMatcher = try XCTUnwrap(core.waitAndReturnRUMEventMatchers().last) + try rumMatcher.assertItFullyMatches( + jsonString: """ + { + "type": "telemetry", + "date": \(1_712_069_357_432 + 123.toInt64Milliseconds), + "service": "browser-rum-sdk", + "version": "5.2.0-b93ed472a4f14fbf2bcd1bc2c9faacb4abbeed82", + "source": "browser", + "_dd": { "format_version": 2 }, + "telemetry": + { + "type": "configuration", + "configuration": + { + "session_replay_sample_rate": 100, + "use_allowed_tracing_urls": false, + "selected_tracing_propagators": [], + "default_privacy_level": "allow", + "use_excluded_activity_urls": false, + "use_worker_url": false, + "track_user_interactions": true, + "track_resources": true, + "track_long_task": true, + "session_sample_rate": 100, + "telemetry_sample_rate": 100, + "use_before_send": false, + "use_proxy": false, + "allow_fallback_to_local_storage": false, + "store_contexts_across_pages": false, + "allow_untrusted_events": false + }, + "runtime_env": { "is_local_file": false, "is_worker": false } + }, + "experimental_features": [], + "application": { "id": "\(randomApplicationID)" }, + "session": { "id": "\(expectedUUID)" }, + "view": {}, + "action": { "id": [] } + } + """ + ) + } +} + +#endif diff --git a/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift new file mode 100644 index 0000000000..ea830d5cf0 --- /dev/null +++ b/Datadog/IntegrationUnitTests/SessionReplay/WebRecordIntegrationTests.swift @@ -0,0 +1,149 @@ +/* + * 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 +#if !os(tvOS) +import WebKit + +import TestUtilities +@testable import DatadogRUM +@testable import DatadogWebViewTracking +@_spi(Internal) +@testable import DatadogSessionReplay + +class WebRecordIntegrationTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + private var core: DatadogCoreProxy! + private var webView: WKWebView! + private var controller: WKUserContentControllerMock! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUp() { + core = DatadogCoreProxy( + context: .mockWith( + env: "test", + version: "1.1.1", + serverTimeOffset: 123 + ) + ) + + controller = WKUserContentControllerMock() + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() // do not share cookies between instances: prevent leak + configuration.userContentController = controller + webView = WKWebView(frame: .zero, configuration: configuration) + WebViewTracking.enable(webView: webView, in: core) + } + + override func tearDown() { + WebViewTracking.disable(webView: webView) + core.flushAndTearDown() + core = nil + webView = nil + controller = nil + } + + func testWebRecordIntegration() throws { + // Given + let randomApplicationID: String = .mockRandom() + let randomUUID: UUID = .mockRandom() + let randomBrowserViewID: UUID = .mockRandom() + + SessionReplay.enable(with: SessionReplay.Configuration(replaySampleRate: 100), 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())" } + } + """ + + // 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())" } + } + """ + + // 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 + ]) + } +} + +#endif diff --git a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift new file mode 100644 index 0000000000..c9034966ca --- /dev/null +++ b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift @@ -0,0 +1,458 @@ +/* + * 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 DatadogTrace +import DatadogInternal +import TestUtilities + +private class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate {} + +class HeadBasedSamplingTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var traceConfig: Trace.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + traceConfig = Trace.Configuration() + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + traceConfig = nil + super.tearDown() + } + + // MARK: - Local Tracing + + func testSamplingLocalTrace() throws { + /* + This is the basic situation of local trace with 3 spans: + + client-ios-app: [-------- parent -----------] | + client-ios-app: [----- child --------] | all 3: keep or drop + client-ios-app: [-- grandchild --] | + */ + let localTraceSampling: SampleRate = 50 // keep or drop + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + let parent = Tracer.shared(in: core).startSpan(operationName: "parent") + let child = Tracer.shared(in: core).startSpan(operationName: "child", childOf: parent.context) + let grandchild = Tracer.shared(in: core).startSpan(operationName: "grandchild", childOf: parent.context) + grandchild.finish() + child.finish() + parent.finish() + + let spans = core.waitAndReturnSpanEvents() + XCTAssertEqual(spans.count, 3, "It must send all spans") + + let allKept = spans.filter({ $0.isKept }).count == 3 + let allDropped = spans.filter({ !$0.isKept }).count == 3 + XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") + } + + func testSamplingLocalTraceWithImplicitParent() throws { + /* + This is the situation of local trace with active span as a parent: + + client-ios-app: [-------- active.span -----] | + client-ios-app: [- child1 -][- child2 -] | all 3: keep or drop + */ + let localTraceSampling: SampleRate = 50 // keep or drop + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + let parent = Tracer.shared(in: core).startSpan(operationName: "parent").setActive() + let child1 = Tracer.shared(in: core).startSpan(operationName: "child 1") + child1.finish() + let child2 = Tracer.shared(in: core).startSpan(operationName: "child 2") + child2.finish() + parent.finish() + + let spans = core.waitAndReturnSpanEvents() + XCTAssertEqual(spans.count, 3, "It must send all spans") + + let allKept = spans.filter({ $0.isKept }).count == 3 + let allDropped = spans.filter({ !$0.isKept }).count == 3 + XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") + } + + // MARK: - Distributed Tracing (through network instrumentation API) + + func testSendingSampledDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { + /* + This is the situation where distributed trace starts with the span created with DatadogTrace network + instrumentation (with no parent): + + dd-sdk-ios: [--- urlsession.request ---] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: SampleRate = 0 // drop all + let distributedTraceSampling: SampleRate = .maxSampleRate // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + traceConfig.urlSessionTracking = .init( + firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: distributedTraceSampling) + ) + Trace.enable(with: traceConfig, in: core) + URLSessionInstrumentation.enable(with: .init(delegateClass: InstrumentedSessionDelegate.self), in: core) + + // When + let request = try sendURLSessionRequest(to: "https://foo.com/request", using: InstrumentedSessionDelegate()) + + // Then + let span = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(span.operationName, "urlsession.request") + XCTAssertEqual(span.samplingRate, 1, "Span must use distributed trace sample rate") + XCTAssertTrue(span.isKept, "Span must be sampled") + + // Then + let expectedTraceIDField = String(span.traceID.idLo) + let expectedSpanIDField = String(span.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + } + + func testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { + /* + This is the situation where distributed trace starts with the span created with DatadogTrace network + instrumentation (with no parent): + + dd-sdk-ios: [--- urlsession.request ---] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: SampleRate = .maxSampleRate // keep all + let distributedTraceSampling: SampleRate = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + traceConfig.urlSessionTracking = .init( + firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: distributedTraceSampling) + ) + Trace.enable(with: traceConfig, in: core) + URLSessionInstrumentation.enable(with: .init(delegateClass: InstrumentedSessionDelegate.self), in: core) + + // When + let request = try sendURLSessionRequest(to: "https://foo.com/request", using: InstrumentedSessionDelegate()) + + // Then + let span = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(span.operationName, "urlsession.request") + XCTAssertEqual(span.samplingRate, 0, "Span must use distributed trace sample rate") + XCTAssertFalse(span.isKept, "Span must be dropped") + + // Then + let expectedTraceIDField = span.traceID.toString(representation: .decimal) + let expectedSpanIDField = span.spanID.toString(representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + func testSendingSampledDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with DatadogTrace network instrumentation: + + client-ios-app: [-------- active.span -----------] keep + dd-sdk-ios: [--- urlsession.request ---] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: SampleRate = .maxSampleRate // keep all + let distributedTraceSampling: SampleRate = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + traceConfig.urlSessionTracking = .init( + firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: distributedTraceSampling) + ) + Trace.enable(with: traceConfig, in: core) + URLSessionInstrumentation.enable(with: .init(delegateClass: InstrumentedSessionDelegate.self), in: core) + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let request = try sendURLSessionRequest(to: "https://foo.com/request", using: InstrumentedSessionDelegate()) + span.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let urlsessionSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "urlsession.request" })) + + XCTAssertEqual(activeSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(activeSpan.isKept, "Span must be sampled") + XCTAssertEqual(urlsessionSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(urlsessionSpan.isKept, "Span must be sampled") + XCTAssertEqual(urlsessionSpan.traceID, activeSpan.traceID) + XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = String(activeSpan.traceID.idLo) + let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + } + + func testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with DatadogTrace network instrumentation: + + client-ios-app: [-------- active.span -----------] drop + dd-sdk-ios: [--- urlsession.request ---] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: SampleRate = 0 // drop all + let distributedTraceSampling: SampleRate = .maxSampleRate // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + traceConfig.urlSessionTracking = .init( + firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: distributedTraceSampling) + ) + Trace.enable(with: traceConfig, in: core) + URLSessionInstrumentation.enable(with: .init(delegateClass: InstrumentedSessionDelegate.self), in: core) + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let request = try sendURLSessionRequest(to: "https://foo.com/request", using: InstrumentedSessionDelegate()) + span.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let urlsessionSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "urlsession.request" })) + + XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(activeSpan.isKept, "Span must not be sampled") + XCTAssertEqual(urlsessionSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(urlsessionSpan.isKept, "Span must not be sampled") + XCTAssertEqual(urlsessionSpan.traceID, activeSpan.traceID) + XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = activeSpan.traceID.toString(representation: .decimal) + let expectedSpanIDField = urlsessionSpan.spanID.toString(representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + // MARK: - Distributed Tracing (through Tracer API) + + func testSendingSampledDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: SampleRate = .maxSampleRate // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + + // Then + let expectedTraceIDField = String(networkSpan.traceID.idLo) + let expectedSpanIDField = String(networkSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + } + + func testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: SampleRate = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + + // Then + let expectedTraceIDField = networkSpan.traceID.toString(representation: .decimal) + let expectedSpanIDField = networkSpan.spanID.toString(representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + func testSendingSampledDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] keep + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: SampleRate = .maxSampleRate // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(activeSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = String(activeSpan.traceID.idLo) + let expectedSpanIDField = String(networkSpan.spanID, representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + } + + func testSendingDroppedDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] drop + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: SampleRate = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(activeSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = activeSpan.traceID.toString(representation: .decimal) + let expectedSpanIDField = networkSpan.spanID.toString(representation: .decimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + // MARK: - Helpers + + /// Sends request to `url` using real `URLSession` instrumented with provided `delegate`. + /// It returns the actual request that was sent to the server which can include additional headers set by the SDK. + private func sendURLSessionRequest(to url: String, using delegate: URLSessionDelegate) throws -> URLRequest { + let server = ServerMock(delivery: .success(response: .mockAny(), data: .mockAny())) + let session = server.getInterceptedURLSession(delegate: delegate) + let taskCompleted = expectation(description: "wait for task completion") + let task = session.dataTask(with: .mockWith(url: URL(string: url)!)) { _, _, _ in taskCompleted.fulfill() } + task.resume() + waitForExpectations(timeout: 5) + + let requests = server.waitAndReturnRequests(count: 1) + return try XCTUnwrap(requests.first) + } +} diff --git a/Datadog/TargetSupport/Datadog/Datadog.h b/Datadog/TargetSupport/Datadog/Datadog.h deleted file mode 100644 index 0653a3453a..0000000000 --- a/Datadog/TargetSupport/Datadog/Datadog.h +++ /dev/null @@ -1,17 +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-2020 Datadog, Inc. -*/ - -#import - -//! Project version number for Datadog. -FOUNDATION_EXPORT double DatadogVersionNumber; - -//! Project version string for Datadog. -FOUNDATION_EXPORT const unsigned char DatadogVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Datadog/TargetSupport/DatadogCore/DatadogCore.h b/Datadog/TargetSupport/DatadogCore/DatadogCore.h new file mode 100644 index 0000000000..cbcb5ef2e9 --- /dev/null +++ b/Datadog/TargetSupport/DatadogCore/DatadogCore.h @@ -0,0 +1,18 @@ +/* +* 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 + +//! Project version number for Datadog. +FOUNDATION_EXPORT double DatadogVersionNumber; + +//! Project version string for Datadog. +FOUNDATION_EXPORT const unsigned char DatadogVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import "ObjcAppLaunchHandler.h" +#import "ObjcExceptionHandler.h" diff --git a/Datadog/TargetSupport/Datadog/Info.plist b/Datadog/TargetSupport/DatadogCore/Info.plist similarity index 100% rename from Datadog/TargetSupport/Datadog/Info.plist rename to Datadog/TargetSupport/DatadogCore/Info.plist diff --git a/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.h b/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.h new file mode 100644 index 0000000000..b56e7b52d1 --- /dev/null +++ b/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.h @@ -0,0 +1,17 @@ +/* +* 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 + +//! Project version number for DatadogCrashReporting. +FOUNDATION_EXPORT double DatadogCrashReportingVersionNumber; + +//! Project version string for DatadogCrashReporting. +FOUNDATION_EXPORT const unsigned char DatadogCrashReportingVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.xcconfig b/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.xcconfig new file mode 100644 index 0000000000..3a0cf1a445 --- /dev/null +++ b/Datadog/TargetSupport/DatadogCrashReporting/DatadogCrashReporting.xcconfig @@ -0,0 +1,5 @@ +// Base configuration for DatadogCrashReporting target. +// Note: all configuration here will be applied to `DatadogCrashReporting.framework` produced by Carthage. + +// Include base config +#include "../xcconfigs/Base.xcconfig" diff --git a/Datadog/TargetSupport/DatadogCrashReporting/Info.plist b/Datadog/TargetSupport/DatadogCrashReporting/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/Datadog/TargetSupport/DatadogCrashReporting/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Datadog/TargetSupport/DatadogCrashReportingTests/DatadogCrashReportingTests.xcconfig b/Datadog/TargetSupport/DatadogCrashReportingTests/DatadogCrashReportingTests.xcconfig new file mode 100644 index 0000000000..e9eda94c64 --- /dev/null +++ b/Datadog/TargetSupport/DatadogCrashReportingTests/DatadogCrashReportingTests.xcconfig @@ -0,0 +1,21 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" + +// Both 'DatadogSDKTesting' and 'DatadogCrashReporting' link PLCR dependency statically, resulting with these runtime warnings: +// +// """ +// objc[50310]: Class PLCrashReportThreadInfo is implemented in both +// /Users/.../DatadogCrashReporting.framework/DatadogCrashReporting (0x10b727208) +// and +// /Users/.../DatadogSDKTesting.framework/DatadogSDKTesting (0x10b5ee928). +// One of the two will be used. Which one is undefined. +// """ +// +// Because some mocks in 'DatadogCrashReportingTests' use PLCR type's subclassing, their runtime cast with 'as? ' could fail depending in which library is found first +// We force 'DatadogCrashReporting' to be linked before linking 'DatadogSDKTesting' so PLCR symbols are found in that library. +OTHER_LDFLAGS= $(inherited) -framework DatadogCrashReporting + +// Add DatadogSDKTesting instrumentation (if available in current environment) + #include? "../xcconfigs/DatadogSDKTesting.local.xcconfig" + + diff --git a/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist b/Datadog/TargetSupport/DatadogCrashReportingTests/Info.plist similarity index 100% rename from Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist rename to Datadog/TargetSupport/DatadogCrashReportingTests/Info.plist diff --git a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig deleted file mode 100644 index a840957170..0000000000 --- a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig +++ /dev/null @@ -1,5 +0,0 @@ -// Get common settings from Datadog.xcconfig -#include "../../../xcconfigs/Datadog.xcconfig" - -// This file is auto generated by pre-build action -#include "MockServerAddress.local.xcconfig" diff --git a/Datadog/TargetSupport/DatadogObjc/DatadogObjc.h b/Datadog/TargetSupport/DatadogObjc/DatadogObjc.h index 8db3bdb949..4d22f0bf49 100644 --- a/Datadog/TargetSupport/DatadogObjc/DatadogObjc.h +++ b/Datadog/TargetSupport/DatadogObjc/DatadogObjc.h @@ -1,7 +1,7 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). -* Copyright 2019-2020 Datadog, Inc. +* Copyright 2019-Present Datadog, Inc. */ #import diff --git a/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h b/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h index 7be31d06b4..922efac85f 100644 --- a/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h +++ b/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h @@ -1,11 +1,8 @@ /* -* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -* This product includes software developed at Datadog (https://www.datadoghq.com/). -* Copyright 2019-2020 Datadog, Inc. -*/ - -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// + * 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 "NSURLSessionBridge.h" +#import "CustomObjcViewController.h" diff --git a/Datadog/TargetSupport/DatadogTests/DatadogTests.xcconfig b/Datadog/TargetSupport/DatadogTests/DatadogTests.xcconfig new file mode 100644 index 0000000000..88c1960e02 --- /dev/null +++ b/Datadog/TargetSupport/DatadogTests/DatadogTests.xcconfig @@ -0,0 +1,5 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" + +// Add DatadogSDKTesting instrumentation (if available in current environment) +#include? "../xcconfigs/DatadogSDKTesting.local.xcconfig" diff --git a/Datadog/TargetSupport/E2E/E2E.xcconfig b/Datadog/TargetSupport/E2E/E2E.xcconfig new file mode 100644 index 0000000000..df49ef8a68 --- /dev/null +++ b/Datadog/TargetSupport/E2E/E2E.xcconfig @@ -0,0 +1,2 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" diff --git a/Datadog/TargetSupport/E2E/Info.plist b/Datadog/TargetSupport/E2E/Info.plist new file mode 100644 index 0000000000..86583a0563 --- /dev/null +++ b/Datadog/TargetSupport/E2E/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + E2EDatadogClientToken + $(E2E_DATADOG_CLIENT_TOKEN) + E2ERUMApplicationID + $(E2E_RUM_APPLICATION_ID) + IsRunningOnCI + $(IS_CI) + + diff --git a/Datadog/TargetSupport/E2EInstrumentationTests/E2EInstrumentationTests.xcconfig b/Datadog/TargetSupport/E2EInstrumentationTests/E2EInstrumentationTests.xcconfig new file mode 100644 index 0000000000..df49ef8a68 --- /dev/null +++ b/Datadog/TargetSupport/E2EInstrumentationTests/E2EInstrumentationTests.xcconfig @@ -0,0 +1,2 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" diff --git a/Shopist/ShopistUITests/Info.plist b/Datadog/TargetSupport/E2EInstrumentationTests/Info.plist similarity index 100% rename from Shopist/ShopistUITests/Info.plist rename to Datadog/TargetSupport/E2EInstrumentationTests/Info.plist diff --git a/Datadog/TargetSupport/E2ETests/E2ETests.xcconfig b/Datadog/TargetSupport/E2ETests/E2ETests.xcconfig new file mode 100644 index 0000000000..df49ef8a68 --- /dev/null +++ b/Datadog/TargetSupport/E2ETests/E2ETests.xcconfig @@ -0,0 +1,2 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" diff --git a/Datadog/TargetSupport/E2ETests/Info.plist b/Datadog/TargetSupport/E2ETests/Info.plist new file mode 100644 index 0000000000..53757676d5 --- /dev/null +++ b/Datadog/TargetSupport/E2ETests/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + E2EDatadogClientToken + $(E2E_DATADOG_CLIENT_TOKEN) + E2ERUMApplicationID + $(E2E_RUM_APPLICATION_ID) + IsRunningOnCI + $(IS_CI) + + diff --git a/Datadog/TargetSupport/Example/Example-Bridging-Header.h b/Datadog/TargetSupport/Example/Example-Bridging-Header.h new file mode 100644 index 0000000000..e9b4f3b828 --- /dev/null +++ b/Datadog/TargetSupport/Example/Example-Bridging-Header.h @@ -0,0 +1,11 @@ +/* +* 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. +*/ + +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "CrashReportingObjcHelpers.h" diff --git a/Datadog/TargetSupport/Example/Example.xcconfig b/Datadog/TargetSupport/Example/Example.xcconfig new file mode 100644 index 0000000000..88c1960e02 --- /dev/null +++ b/Datadog/TargetSupport/Example/Example.xcconfig @@ -0,0 +1,5 @@ +// Add common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" + +// Add DatadogSDKTesting instrumentation (if available in current environment) +#include? "../xcconfigs/DatadogSDKTesting.local.xcconfig" diff --git a/Datadog/TargetSupport/Example/Info.plist b/Datadog/TargetSupport/Example/Info.plist index e7d2d49731..c7fe99ec2e 100644 --- a/Datadog/TargetSupport/Example/Info.plist +++ b/Datadog/TargetSupport/Example/Info.plist @@ -2,6 +2,12 @@ + CustomLogsURL + $(CUSTOM_LOGS_URL) + CustomTraceURL + $(CUSTOM_TRACE_URL) + CustomRUMURL + $(CUSTOM_RUM_URL) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,10 +28,20 @@ $(DATADOG_CLIENT_TOKEN) LSRequiresIPhoneOS + NSLocationAlwaysAndWhenInUseUsageDescription + Location updates are used to test SDK support for tracking events while the app is in background. (always and when in use) + NSLocationAlwaysUsageDescription + Location updates are used to test SDK support for tracking events while the app is in background. (always) + NSLocationWhenInUseUsageDescription + Location updates are used to test SDK support for tracking events while the app is in background. (when in use) + RUMApplicationID + $(RUM_APPLICATION_ID) + UIBackgroundModes + + location + UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/DatadogAlamofireExtension.podspec b/DatadogAlamofireExtension.podspec new file mode 100644 index 0000000000..c725e4ff30 --- /dev/null +++ b/DatadogAlamofireExtension.podspec @@ -0,0 +1,33 @@ +Pod::Spec.new do |s| + s.name = "DatadogAlamofireExtension" + 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" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "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' + + s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } + + s.source_files = ["DatadogExtensions/Alamofire/**/*.swift"] + s.dependency 'DatadogInternal', s.version.to_s + s.dependency 'Alamofire', '~> 5.0' +end diff --git a/DatadogCore.podspec b/DatadogCore.podspec new file mode 100644 index 0000000000..3c66db090d --- /dev/null +++ b/DatadogCore.podspec @@ -0,0 +1,33 @@ +Pod::Spec.new do |s| + s.name = "DatadogCore" + s.version = "2.22.0" + s.summary = "Official Datadog Swift SDK for iOS." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + 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 } + + s.source_files = ["DatadogCore/Sources/**/*.swift", + "DatadogCore/Private/**/*.{h,m}"] + + s.resource_bundle = { + "DatadogCore" => "DatadogCore/Resources/PrivacyInfo.xcprivacy" + } + + s.dependency 'DatadogInternal', s.version.to_s + +end diff --git a/DatadogCore/Private/ObjcAppLaunchHandler.m b/DatadogCore/Private/ObjcAppLaunchHandler.m new file mode 100644 index 0000000000..bea30ced2a --- /dev/null +++ b/DatadogCore/Private/ObjcAppLaunchHandler.m @@ -0,0 +1,145 @@ +/* +* 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 + +#import "ObjcAppLaunchHandler.h" + +#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. +// We consider 30s as a threshold for pre-warm detection. +#define COLD_START_TIME_THRESHOLD 30 + +/// Get the process start time from kernel. +/// +/// The time interval is related to the 1 January 2001 00:00:00 GMT reference date. +/// +/// - Parameter timeInterval: Pointer to time interval to hold the process start time interval. +int processStartTimeIntervalSinceReferenceDate(NSTimeInterval *timeInterval); + +/// `AppLaunchHandler` aims to track some times as part of the sequence +/// described in Apple's "About the App Launch Sequence" +/// +/// ref. https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence +@implementation __dd_private_AppLaunchHandler { + NSTimeInterval _processStartTime; + NSTimeInterval _timeToApplicationDidBecomeActive; + BOOL _isActivePrewarm; + UIApplicationDidBecomeActiveCallback _applicationDidBecomeActiveCallback; +} + +/// Shared instance of the Application Launch Handler. +static __dd_private_AppLaunchHandler *_shared; + ++ (void)load { + // This is called at the `DatadogPrivate` load time, keep the work minimal + _shared = [[self alloc] initWithProcessInfo:NSProcessInfo.processInfo + loadTime:CFAbsoluteTimeGetCurrent()]; + + 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:notificationName + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *_){ + + @synchronized(_shared) { + NSTimeInterval time = CFAbsoluteTimeGetCurrent() - _shared->_processStartTime; + _shared->_timeToApplicationDidBecomeActive = time; + _shared->_applicationDidBecomeActiveCallback(time); + } + + [center removeObserver:token]; + token = nil; + }]; +#endif +} + ++ (__dd_private_AppLaunchHandler *)shared { + return _shared; +} + +- (instancetype)initWithProcessInfo:(NSProcessInfo *)processInfo loadTime:(NSTimeInterval)loadTime { + NSTimeInterval startTime; + if (processStartTimeIntervalSinceReferenceDate(&startTime) != 0) { + // fallback on the loading time + startTime = loadTime; + } + + // The ActivePrewarm variable indicates whether the app was launched via pre-warming. + BOOL isActivePrewarm = [processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"]; + return [self initWithStartTime:startTime isActivePrewarm:isActivePrewarm]; +} + +- (instancetype)initWithStartTime:(NSTimeInterval)startTime isActivePrewarm:(BOOL)isActivePrewarm { + self = [super init]; + if (!self) return nil; + _processStartTime = startTime; + _isActivePrewarm = isActivePrewarm; + _applicationDidBecomeActiveCallback = ^(NSTimeInterval _) {}; + return self; +} + +- (NSDate *)launchDate { + @synchronized(self) { + return [NSDate dateWithTimeIntervalSinceReferenceDate:_processStartTime]; + } +} + +- (NSNumber *)launchTime { + @synchronized(self) { + return _timeToApplicationDidBecomeActive > 0 ? + @(_timeToApplicationDidBecomeActive) : nil; + } +} + +- (BOOL)isActivePrewarm { + @synchronized(self) { + if (_isActivePrewarm) return _isActivePrewarm; + return _timeToApplicationDidBecomeActive > COLD_START_TIME_THRESHOLD; + } +} + +- (void)setApplicationDidBecomeActiveCallback:(UIApplicationDidBecomeActiveCallback)callback { + @synchronized(self) { + _applicationDidBecomeActiveCallback = callback; + } +} + +@end + +int processStartTimeIntervalSinceReferenceDate(NSTimeInterval *timeInterval) { + // Query the current process' start time: + // https://www.freebsd.org/cgi/man.cgi?sysctl(3) + // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/sysctl.h#L681 + // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/proc.h#L97 + struct kinfo_proc kip; + size_t kipSize = sizeof(kip); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + int res = sysctl(mib, 4, &kip, &kipSize, NULL, 0); + if (res != 0) return res; + + // The process' start time is provided relative to 1 Jan 1970 + struct timeval startTime = kip.kp_proc.p_starttime; + // Multiplication with 1.0 ensure we don't round to 0 with integer division + NSTimeInterval processStartTime = startTime.tv_sec + (1.0 * startTime.tv_usec) / USEC_PER_SEC; + // Convert to time since 1 Jan 2001 to align with CFAbsoluteTimeGetCurrent() + *timeInterval = processStartTime - kCFAbsoluteTimeIntervalSince1970; + return res; +} diff --git a/DatadogCore/Private/ObjcExceptionHandler.m b/DatadogCore/Private/ObjcExceptionHandler.m new file mode 100644 index 0000000000..d287fdcf2b --- /dev/null +++ b/DatadogCore/Private/ObjcExceptionHandler.m @@ -0,0 +1,23 @@ +/* +* 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 "ObjcExceptionHandler.h" + +@implementation __dd_private_ObjcExceptionHandler + ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error { + @try { + tryBlock(); + return YES; + } + @catch (NSException *exception) { + *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo]; + return NO; + } +} + +@end diff --git a/DatadogCore/Private/include/ObjcAppLaunchHandler.h b/DatadogCore/Private/include/ObjcAppLaunchHandler.h new file mode 100644 index 0000000000..cba4edab14 --- /dev/null +++ b/DatadogCore/Private/include/ObjcAppLaunchHandler.h @@ -0,0 +1,48 @@ +/* +* 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 + +NS_ASSUME_NONNULL_BEGIN + +/// `AppLaunchHandler` aims to track some times as part of the sequence +/// described in Apple's "About the App Launch Sequence" +/// +/// ref. https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence +@interface __dd_private_AppLaunchHandler : NSObject + +typedef void (^UIApplicationDidBecomeActiveCallback) (NSTimeInterval); + +/// Sole instance of the Application Launch Handler. +@property (class, readonly) __dd_private_AppLaunchHandler *shared; + +/// Returns the Application process launch date. +@property (atomic, readonly) NSDate* launchDate; + +/// Returns the time interval in seconds between startup of the application process and the +/// `UIApplicationDidBecomeActiveNotification`. Or `nil` If the +/// `UIApplicationDidBecomeActiveNotification` has not been reached yet. +@property (atomic, readonly, nullable) NSNumber* launchTime; + +/// Returns `true` when the application is pre-warmed. +/// +/// System sets environment variable `ActivePrewarm` to 1 when app is pre-warmed. +@property (atomic, readonly) BOOL isActivePrewarm; + +/// Sets the callback to be invoked when the application becomes active. +/// +/// The closure get the updated handler as argument. You will not get any +/// notification if the application became active before setting the callback +/// +/// - Parameter callback: The callback closure. +- (void)setApplicationDidBecomeActiveCallback:(UIApplicationDidBecomeActiveCallback)callback; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DatadogCore/Private/include/ObjcExceptionHandler.h b/DatadogCore/Private/include/ObjcExceptionHandler.h new file mode 100644 index 0000000000..ed1c57e981 --- /dev/null +++ b/DatadogCore/Private/include/ObjcExceptionHandler.h @@ -0,0 +1,18 @@ +/* +* 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface __dd_private_ObjcExceptionHandler : NSObject + ++ (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error + NS_SWIFT_NAME(rethrow(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/DatadogCore/Resources/PrivacyInfo.xcprivacy b/DatadogCore/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..f3461fdf14 --- /dev/null +++ b/DatadogCore/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,32 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePerformanceData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift new file mode 100644 index 0000000000..34f5530e6b --- /dev/null +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -0,0 +1,123 @@ +/* + * 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 canImport(UIKit) +import UIKit +import DatadogInternal +#if canImport(WatchKit) +import WatchKit +#endif + +internal final class ApplicationStatePublisher: ContextValuePublisher { + typealias Snapshot = AppStateHistory.Snapshot + + /// The default publisher queue. + private static let defaultQueue = DispatchQueue( + label: "com.datadoghq.app-state-publisher", + target: .global(qos: .utility) + ) + + /// The initial history value. + let initialValue: AppStateHistory + + /// The notification center where this publisher observes following `UIApplication` notifications: + /// - `.didBecomeActiveNotification` + /// - `.willResignActiveNotification` + /// - `.didEnterBackgroundNotification` + /// - `.willEnterForegroundNotification` + private let notificationCenter: NotificationCenter + + /// The date provider for the Application state snapshot timestamp. + private let dateProvider: DateProvider + + /// The queue used to serialise access to the `history` and + /// to publish the new history. + private let queue: DispatchQueue + + /// The current application state history. + /// + /// To mutate in the `queue` only. + private var history: AppStateHistory + + /// The receiver for publishing the state history. + /// + /// To mutate in the `queue` only. + private var receiver: ContextValueReceiver? + + /// Creates a Application state publisher for publishing application state + /// history. + /// + /// **Note**: It must be called on the main thread. + /// + /// - Parameters: + /// - 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( + appStateProvider: AppStateProvider, + notificationCenter: NotificationCenter, + dateProvider: DateProvider, + queue: DispatchQueue = ApplicationStatePublisher.defaultQueue + ) { + let initialValue = AppStateHistory( + initialState: appStateProvider.current, + date: dateProvider.now + ) + + self.initialValue = initialValue + self.history = initialValue + self.queue = queue + self.dateProvider = dateProvider + self.notificationCenter = notificationCenter + } + + func publish(to receiver: @escaping ContextValueReceiver) { + queue.async { self.receiver = receiver } + 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 + private func applicationDidBecomeActive() { + append(state: .active) + } + + @objc + private func applicationWillResignActive() { + append(state: .inactive) + } + + @objc + private func applicationDidEnterBackground() { + append(state: .background) + } + + @objc + private func applicationWillEnterForeground() { + append(state: .inactive) + } + + private func append(state: AppState) { + let snapshot = Snapshot(state: state, date: dateProvider.now) + queue.async { + self.history.append(snapshot) + self.receiver?(self.history) + } + } + + func cancel() { + 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 } + } +} + +#endif diff --git a/DatadogCore/Sources/Core/Context/ApplicationVersionPublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationVersionPublisher.swift new file mode 100644 index 0000000000..5a07feb8f4 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/ApplicationVersionPublisher.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 + +/// Publishes the application vesrion value to receiver. +internal final class ApplicationVersionPublisher: ContextValuePublisher { + let initialValue: String + + private var receiver: ContextValueReceiver? + + var version: String { + didSet { receiver?(version) } + } + + init(version: String) { + self.initialValue = version + self.version = version + } + + func publish(to receiver: @escaping ContextValueReceiver) { + self.receiver = receiver + } + + func cancel() { + receiver = nil + } +} diff --git a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift new file mode 100644 index 0000000000..1b1c529ff3 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift @@ -0,0 +1,101 @@ +/* + * 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 + +#if os(iOS) +import UIKit + +/// The ``BatteryStatusPublisher`` publishes the battery state and level from the ``UIDevice``. +/// +/// The publisher will enable the battery monitoring by setting the ``UIDevice/isBatteryMonitoringEnabled`` +/// to `true`. The property will be reset to it's initial value when the publisher is deallocated. +internal final class BatteryStatusPublisher: ContextValuePublisher { + let initialValue: BatteryStatus? + let device: UIDevice + let isBatteryMonitoringEnabled: Bool + private let notificationCenter: NotificationCenter + private var observers: [Any]? = nil + + /// Creates a battery status publisher from the given device. + /// + /// - Parameters: + /// - notificationCenter: The notification center for observing the `UIDevice` battery changes, + /// - device: The `UIDevice` instance. + init( + notificationCenter: NotificationCenter, + device: UIDevice + ) { + self.device = device + self.notificationCenter = notificationCenter + self.isBatteryMonitoringEnabled = device.isBatteryMonitoringEnabled + self.initialValue = BatteryStatus( + state: .init(device.batteryState), + level: device.batteryLevel + ) + + device.isBatteryMonitoringEnabled = true + } + + func publish(to receiver: @escaping ContextValueReceiver) { + let block = { (notification: Notification) in + guard let device = notification.object as? UIDevice else { + return + } + + let status = BatteryStatus( + state: .init(device.batteryState), + level: device.batteryLevel + ) + + receiver(status) + } + + observers = [ + notificationCenter.addObserver( + forName: UIDevice.batteryStateDidChangeNotification, + object: device, + queue: .main, + using: block + ), + notificationCenter.addObserver( + forName: UIDevice.batteryLevelDidChangeNotification, + object: device, + queue: .main, + using: block + ) + ] + } + + func cancel() { + device.isBatteryMonitoringEnabled = isBatteryMonitoringEnabled + observers?.forEach(notificationCenter.removeObserver) + observers = nil + } +} + +extension BatteryStatus.State { + /// Cast `UIDevice.BatteryState` to `BatteryStatus.State` + /// + /// - Parameter state: The state to cast. + init(_ state: UIDevice.BatteryState) { + switch state { + case .unknown: + self = .unknown + case .unplugged: + self = .unplugged + case .charging: + self = .charging + case .full: + self = .full + @unknown default: + self = .unknown + } + } +} + +#endif diff --git a/DatadogCore/Sources/Core/Context/CarrierInfoPublisher.swift b/DatadogCore/Sources/Core/Context/CarrierInfoPublisher.swift new file mode 100644 index 0000000000..789fd89534 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/CarrierInfoPublisher.swift @@ -0,0 +1,79 @@ +/* + * 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 + +#if os(iOS) && !targetEnvironment(macCatalyst) && !(swift(>=5.9) && os(visionOS)) + +import CoreTelephony + +/// It reads `CarrierInfo?` from `CTTelephonyNetworkInfo` only when `CTCarrier` has changed (e.g. when the SIM card was swapped). +internal struct CarrierInfoPublisher: ContextValuePublisher { + let initialValue: CarrierInfo? + + private let networkInfo: CTTelephonyNetworkInfo + + init(networkInfo: CTTelephonyNetworkInfo = .init()) { + self.networkInfo = networkInfo + self.initialValue = CarrierInfo(networkInfo, service: networkInfo.serviceCurrentRadioAccessTechnology?.keys.first) + } + + func publish(to receiver: @escaping ContextValueReceiver) { + // The `serviceSubscriberCellularProvidersDidUpdateNotifier` block object executes on the default priority + // global dispatch queue when the user’s cellular provider information changes. + // This occurs, for example, if a user swaps the device’s SIM card with one from another provider, while the app is running. + // ref.: https://developer.apple.com/documentation/coretelephony/cttelephonynetworkinfo/3024512-servicesubscribercellularprovide + networkInfo.serviceSubscriberCellularProvidersDidUpdateNotifier = { key in + // On iOS12+ `CarrierInfo` subscribers are notified on actual change to cellular provider. + let info = CarrierInfo(self.networkInfo, service: key) + receiver(info) + } + } + + func cancel() { + networkInfo.serviceSubscriberCellularProvidersDidUpdateNotifier = nil + } +} + +extension CarrierInfo { + init?(_ info: CTTelephonyNetworkInfo, service key: String?) { + guard let key = key, + let radioTechnology = info.serviceCurrentRadioAccessTechnology?[key], + let carrier = info.serviceSubscriberCellularProviders?[key] + else { + return nil // the service is not registered on any network + } + + self.init( + carrierName: carrier.carrierName, + carrierISOCountryCode: carrier.isoCountryCode, + carrierAllowsVOIP: carrier.allowsVOIP, + radioAccessTechnology: .init(radioTechnology) + ) + } +} + +extension CarrierInfo.RadioAccessTechnology { + init(_ radioAccessTechnology: String) { + switch radioAccessTechnology { + case CTRadioAccessTechnologyGPRS: self = .GPRS + case CTRadioAccessTechnologyEdge: self = .Edge + case CTRadioAccessTechnologyWCDMA: self = .WCDMA + case CTRadioAccessTechnologyHSDPA: self = .HSDPA + case CTRadioAccessTechnologyHSUPA: self = .HSUPA + case CTRadioAccessTechnologyCDMA1x: self = .CDMA1x + case CTRadioAccessTechnologyCDMAEVDORev0: self = .CDMAEVDORev0 + case CTRadioAccessTechnologyCDMAEVDORevA: self = .CDMAEVDORevA + case CTRadioAccessTechnologyCDMAEVDORevB: self = .CDMAEVDORevB + case CTRadioAccessTechnologyeHRPD: self = .eHRPD + case CTRadioAccessTechnologyLTE: self = .LTE + default: self = .unknown + } + } +} + +#endif diff --git a/DatadogCore/Sources/Core/Context/ContextValuePublisher.swift b/DatadogCore/Sources/Core/Context/ContextValuePublisher.swift new file mode 100644 index 0000000000..f394aad3d6 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/ContextValuePublisher.swift @@ -0,0 +1,219 @@ +/* + * 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 receiver closure for receiving new values. +internal typealias ContextValueReceiver = (Value) -> Void + +// MARK: - Publisher + +/// Declares that a type can transmit a sequence of values over time. +/// +/// The receiver's ``ContextValueReceiver/Value`` generic type must match the +/// ``ContextValuePublisher/Value`` types declared by the publisher. +/// +/// A publisher delivers elements to one ``ContextValueReceiver`` closure. After this, +/// the publisher can call the receiver with new values. At anytime, a subcriber can cancel the +/// subscription. Invoking `cancel()` must stop calling its downstream receiver, canceling +/// should also eliminate any strong references it currently holds. +/// +/// The publisher provides an `initialValue`, this parameter must be immutable, thead-safe, +/// and it shouldn't block the caller. +/// +/// Every ``ContextValuePublisher`` must adhere to this contract for downstream +/// subscribers to function correctly. +internal protocol ContextValuePublisher { + /// The kind of values published by this publisher. + associatedtype Value + + /// The initial value of the publisher. + var initialValue: Value { get } + + /// Start sending values to a receiver. + func publish(to receiver: @escaping ContextValueReceiver) + + /// Cancels publications. + /// + /// When implementing ``ContextValueSubscription`` in support of a custom publisher, + /// implement `cancel()` to request that your publisher stop calling its downstream receiver. + /// It's not required that the publisher stop immediately, but canceling should also eliminate any + /// strong references it currently holds. + /// + /// After you receive one call to `cancel()`, subsequent calls shouldn't do anything. Additionally, + /// your implementation must be thread-safe, and it shouldn't block the caller. + func cancel() +} + +// MARK: - Subscription + +/// A protocol representing the connection of a receiver to a publisher. +/// +/// Canceling a ``ContextValueSubscription`` must be thread-safe and you can only cancel a +/// ``ContextValueSubscription`` once. +/// +/// Canceling a subscription frees up any resources previously allocated by attaching the +/// ``ContextValueReceiver``. +internal protocol ContextValueSubscription { + /// Cancels the subcription. + /// + /// When implementing ``ContextValueSubscription`` in support of a custom publisher, + /// implement `cancel()` to request that your publisher stop calling its downstream receiver. + /// It's not required that the publisher stop immediately, but canceling should also eliminate any + /// strong references it currently holds. + /// + /// After you receive one call to `cancel()`, subsequent calls shouldn't do anything. Additionally, + /// your implementation must be thread-safe, and it shouldn't block the caller. + func cancel() +} + +/// Attaches a ``ContextValueReceiver`` to a ``ContextValuePublisher`` to create +/// ``ContextValueSubscription`` instance. +/// +/// An instance of ``ContextValueBlockSubscription`` is returned when subrcribing to a +/// publisher ``ContextValuePublisher/subscribe``. +/// +/// let subscription = publisher.subscribe { value in +/// print(value) +/// } +/// +/// subscription.cancel() +/// +/// After cancelling the subscription, the publisher is released. +private class ContextValueBlockSubscription: ContextValueSubscription where Publisher: ContextValuePublisher { + private var publisher: Publisher? + + /// Creates a subscription but subscribing the receiver to the publisher. + /// + /// At initialization, the publisher will start publishing to the receiver. + /// + /// - Parameters: + /// - publisher: The publisher to subscribe. + /// - receiver: The receiver closure. + init(_ publisher: Publisher, receiver: @escaping ContextValueReceiver) { + self.publisher = publisher + self.publisher?.publish(to: receiver) + } + + /// Cancels the publication and free up allocated memory + func cancel() { + publisher?.cancel() + publisher = nil + } +} + +extension ContextValuePublisher { + /// Subscribes the receiver to the receiver. + /// + /// After subscription, the publisher will start invoking the receiver with new values. + /// + /// let subscription = publisher.subscribe { value in + /// print(value) + /// } + /// + /// When no more values are required, the subscription can be cancelled. + /// + /// subscription.cancel() + /// + /// - Parameter receiver: The receiver closure. + /// - Returns: A subscription instance. + func subscribe(_ receiver: @escaping ContextValueReceiver) -> ContextValueSubscription { + return ContextValueBlockSubscription(self, receiver: receiver) + } +} + +// MARK: - Type-Erasure + +/// A publisher that performs type erasure by wrapping another publisher. +/// +/// ``AnyContextValuePublisher`` is a concrete implementation of ``ContextValuePublisher`` +/// that has no significant properties of its own, and passes through values from its upstream +/// publisher. +/// +/// Use ``AnyContextValuePublisher`` to wrap a publisher whose type has details +/// you don’t want to expose across API boundaries, such as different modules +/// +/// You can use extension method ``ContextValuePublish/ereraseToAnyPublisher()`` +/// operator o wrap a publisher with ``AnyContextValuePublisher``. +internal struct AnyContextValuePublisher: ContextValuePublisher { + /// The initial value of the publisher. + let initialValue: Value + + private let publishBlock: (@escaping ContextValueReceiver) -> Void + private let cancelBlock: () -> Void + + /// Creates a type-erasing publisher to wrap the provided publisher. + /// + /// - Parameter publisher: A publisher to wrap with a type-eraser. + init(_ publisher: Publisher) where Publisher: ContextValuePublisher, Publisher.Value == Value { + initialValue = publisher.initialValue + publishBlock = publisher.publish + cancelBlock = publisher.cancel + } + + /// Tells a publisher that it may send more values to the subscriber. + func publish(to receiver: @escaping ContextValueReceiver) { + self.publishBlock(receiver) + } + + /// Cancels publications. + /// + /// When implementing ``ContextValueSubscription`` in support of a custom publisher, + /// implement `cancel()` to request that your publisher stop calling its downstream receiver. + /// It's not required that the publisher stop immediately, but canceling should also eliminate any + /// strong references it currently holds. + /// + /// After you receive one call to `cancel()`, subsequent calls shouldn't do anything. Additionally, + /// your implementation must be thread-safe, and it shouldn't block the caller. + func cancel() { + self.cancelBlock() + } +} + +extension ContextValuePublisher { + /// Wraps this publisher with a type eraser. + /// + /// Use ``ContextValuePublish/eraseToAnyPublisher()`` to expose an instance of + /// ``AnyContextValuePublisher`` to the downstream subscriber, rather than this publisher’s + /// actual type. This form of _type erasure_ preserves abstraction across API boundaries. + /// + /// - Returns: An ``AnyContextValuePublisher`` wrapping this publisher. + func eraseToAnyPublisher() -> AnyContextValuePublisher { + return AnyContextValuePublisher(self) + } +} + +// MARK: - No-op + +/// A no-operation publisher. +/// +/// ``NOPContextValuePublisher`` is a concrete implementation of ``ContextValuePublisher`` +/// that has no effect when invoking ``ContextValuePublisher/read``. +/// +/// You can use ``NOContextValuePublisher`` as a placeholder. +internal struct NOPContextValuePublisher: ContextValuePublisher { + /// The initial value of the reader. + let initialValue: Value + + /// Creates a type-erasing reader to wrap the provided reader. + /// + /// - Parameter reader: A reader to wrap with a type-eraser. + init(initialValue: Value) { + self.initialValue = initialValue + } + + init() where Value: ExpressibleByNilLiteral { + self.initialValue = nil + } + + func publish(to receiver: @escaping ContextValueReceiver) { + // no-op + } + + func cancel() { + // no-op + } +} diff --git a/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift new file mode 100644 index 0000000000..61980e0cf0 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/DatadogContextProvider.swift @@ -0,0 +1,145 @@ +/* + * 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 + +/// Provides thread-safe access to Datadog Context. +/// +/// The context can be accessed asynchronously for reads and writes. +/// +/// provider.read { context in +/// // read value from the current context +/// } +/// +/// provider.write { context in +/// // set mutable values of the context +/// } +/// +/// The provider performs reads concurrently but uses barrier block for +/// write operations. +/// +/// 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(initialValue: 0) +/// provider.assign(reader: reader, to: \.serverTimeOffset) +/// +/// +/// The context provider can subscribe a context property to a publisher that complies +/// to ``ContextValuePublisher``. e.g.: +/// +/// let publisher = ServerOffsetPublisher(initialValue: 0) +/// provider.subscribe(\.serverTimeOffset, to: publisher) +/// +/// All subscriptions will be cancelled when the provider is deallocated. +internal final class DatadogContextProvider { + /// The current `context`. + /// + /// The value must be accessed from the `queue` only. + private var context: DatadogContext + + /// The queue used to synchronize the access to the `DatadogContext`. + internal let queue = DispatchQueue( + label: "com.datadoghq.core-context", + qos: .utility + ) + + /// List of receivers to invoke when the context changes. + private var receivers: [ContextValueReceiver] + + /// List of subscription of context values. + private var subscriptions: [ContextValueSubscription] + + /// Creates a context provider to perform reads and writes on the + /// shared Datadog context. + /// + /// - Parameter context: The inital context value. + init(context: DatadogContext) { + self.context = context + self.receivers = [] + self.subscriptions = [] + } + + deinit { + subscriptions.forEach { $0.cancel() } + } + + /// Publishes context changes to the given receiver. + /// + /// - Parameter receiver: The receiver closure. + func publish(to receiver: @escaping ContextValueReceiver) { + queue.async { self.receivers.append(receiver) } + } + + /// Reads to the `context` synchronously, by blocking the caller thread. + /// + /// **Warning:** This method will block the caller thread by reading the context + /// synchronously on a concurrent queue. + /// + /// - Returns: The current context. + func read() -> DatadogContext { + 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.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) + self.receivers.forEach { receiver in + receiver(self.context) + } + } + } + + /// Subscribes a context's property to a publisher. + /// + /// The context provider can subscribe a context property to a publisher that complies + /// to ``ContextValuePublisher``. e.g.: + /// + /// let publisher = ServerOffsetPublisher(initialValue: 0) + /// provider.subscribe(\.serverTimeOffset, to: publisher) + /// + /// - Parameters: + /// - keyPath: A context's key path that supports reading from and writing to the resulting value. + /// - publisher: The context value publisher. + func subscribe(_ keyPath: WritableKeyPath, to publisher: Publisher) where Publisher: ContextValuePublisher { + let subscription = publisher.subscribe { [weak self] value in + self?.write { $0[keyPath: keyPath] = value } + } + + write { + $0[keyPath: keyPath] = publisher.initialValue + self.subscriptions.append(subscription) + } + } + +#if DD_SDK_COMPILED_FOR_TESTING + func replace(context newContext: DatadogContext) { + queue.async { + self.context = newContext + } + } +#endif +} + +extension DatadogContextProvider: Flushable { + /// Awaits completion of all asynchronous operations. + /// + /// **blocks the caller thread** + func flush() { + queue.sync { } + } +} diff --git a/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift b/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift new file mode 100644 index 0000000000..26b860049c --- /dev/null +++ b/DatadogCore/Sources/Core/Context/LaunchTimePublisher.swift @@ -0,0 +1,46 @@ +/* + * 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(macOS) +import Foundation +import DatadogInternal + +#if SPM_BUILD +import DatadogPrivate +#endif + +internal struct LaunchTimePublisher: ContextValuePublisher { + private typealias AppLaunchHandler = __dd_private_AppLaunchHandler + + let initialValue: LaunchTime? + + init() { + initialValue = LaunchTime( + launchTime: AppLaunchHandler.shared.launchTime?.doubleValue, + launchDate: AppLaunchHandler.shared.launchDate, + isActivePrewarm: AppLaunchHandler.shared.isActivePrewarm + ) + } + + func publish(to receiver: @escaping ContextValueReceiver) { + let launchDate = AppLaunchHandler.shared.launchDate + let isActivePrewarm = AppLaunchHandler.shared.isActivePrewarm + + AppLaunchHandler.shared.setApplicationDidBecomeActiveCallback { launchTime in + let value = LaunchTime( + launchTime: launchTime, + launchDate: launchDate, + isActivePrewarm: isActivePrewarm + ) + receiver(value) + } + } + + func cancel() { + AppLaunchHandler.shared.setApplicationDidBecomeActiveCallback { _ in } + } +} +#endif diff --git a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift new file mode 100644 index 0000000000..7e314439fe --- /dev/null +++ b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift @@ -0,0 +1,56 @@ +/* + * 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 + +/// The low power mode publisher will publish the ``ProcessInfo/isLowPowerModeEnabled`` value +/// by observing the `NSProcessInfoPowerStateDidChange` notification on the given +/// notification center. +internal final class LowPowerModePublisher: ContextValuePublisher { + let initialValue: Bool + + private let notificationCenter: NotificationCenter + private var observer: Any? + + /// Creates a low power mode publisher. + /// + /// - Parameters: + /// - notificationCenter: The notification center for observing the `NSProcessInfoPowerStateDidChange`, + /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. + init( + notificationCenter: NotificationCenter, + processInfo: ProcessInfo + ) { + self.initialValue = processInfo.isLowPowerModeEnabled + self.notificationCenter = notificationCenter + } + + func publish(to receiver: @escaping ContextValueReceiver) { + self.observer = notificationCenter + .addObserver( + forName: .NSProcessInfoPowerStateDidChange, + object: nil, + queue: .main + ) { notification in + guard let processInfo = notification.object as? ProcessInfo else { + return + } + + // We suspect an iOS 15 bug (ref.: https://openradar.appspot.com/FB9741207) which leads to rare + // `_os_unfair_lock_recursive_abort` crash when `processInfo.isLowPowerModeEnabled` is accessed + // directly in the notification handler. As a workaround, we defer its access to the next run loop + // where underlying lock should be already released. + OperationQueue.main.addOperation { + receiver(processInfo.isLowPowerModeEnabled) + } + } + } + + func cancel() { + observer.map(notificationCenter.removeObserver) + } +} diff --git a/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift b/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift new file mode 100644 index 0000000000..4afafb8e3e --- /dev/null +++ b/DatadogCore/Sources/Core/Context/NetworkConnectionInfoPublisher.swift @@ -0,0 +1,94 @@ +/* + * 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 Network + +/// Thread-safe wrapper for `NWPathMonitor`. +/// +/// The `NWPathMonitor` provides two models of getting the `NWPath` info: +/// * pulling the value with `monitor.currentPath`, +/// * pushing the value with `monitor.pathUpdateHandler = { path in ... }`. +/// +/// We found the pulling model to not be thread-safe: accessing `currentPath` properties lead to occasional crashes. +/// The `ThreadSafeNWPathMonitor` listens to path updates and synchonizes the values on `.current` property. +/// This adds the necessary thread-safety and keeps the convenience of pulling. +internal struct NWPathMonitorPublisher: ContextValuePublisher { + private static let defaultQueue = DispatchQueue( + label: "com.datadoghq.nw-path-monitor-publisher", + target: .global(qos: .utility) + ) + + let initialValue: NetworkConnectionInfo? + + private let monitor: NWPathMonitor + private let queue: DispatchQueue + + init( + monitor: NWPathMonitor = .init(), + queue: DispatchQueue = NWPathMonitorPublisher.defaultQueue + ) { + self.monitor = monitor + self.queue = queue + self.initialValue = NetworkConnectionInfo(monitor.currentPath) + } + + func publish(to receiver: @escaping ContextValueReceiver) { + monitor.pathUpdateHandler = { + let info = NetworkConnectionInfo($0) + receiver(info) + } + + monitor.start(queue: queue) + } + + func cancel() { + monitor.cancel() + } +} + +extension NetworkConnectionInfo { + init(_ path: NWPath) { + self.init( + reachability: NetworkConnectionInfo.Reachability(path.status), + availableInterfaces: path.availableInterfaces.map { .init($0.type) }, + supportsIPv4: path.supportsIPv4, + supportsIPv6: path.supportsIPv6, + isExpensive: path.isExpensive, + isConstrained: { + guard #available(iOS 13, tvOS 13, *) else { + return nil + } + return path.isConstrained + }() + ) + } +} + +extension NetworkConnectionInfo.Reachability { + init(_ status: NWPath.Status) { + switch status { + case .satisfied: self = .yes + case .requiresConnection: self = .maybe + case .unsatisfied: self = .no + @unknown default: self = .maybe + } + } +} + +extension NetworkConnectionInfo.Interface { + init(_ interface: NWInterface.InterfaceType) { + switch interface { + case .wifi: self = .wifi + case .wiredEthernet: self = .wiredEthernet + case .cellular: self = .cellular + case .loopback: self = .loopback + case .other: self = .other + @unknown default: self = .other + } + } +} diff --git a/DatadogCore/Sources/Core/Context/ServerOffsetPublisher.swift b/DatadogCore/Sources/Core/Context/ServerOffsetPublisher.swift new file mode 100644 index 0000000000..43a45e5d90 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/ServerOffsetPublisher.swift @@ -0,0 +1,106 @@ +/* + * 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 + +/// List of Datadog NTP pools. +public let DatadogNTPServers = [ + "0.datadog.pool.ntp.org", + "1.datadog.pool.ntp.org", + "2.datadog.pool.ntp.org", + "3.datadog.pool.ntp.org" +] + +/// Abstract the monotonic clock synchronized with the server using NTP. +public protocol ServerDateProvider { + /// Start the clock synchronisation with NTP server. + /// + /// Calls the `completion` by passing it the server time offset when the synchronization succeeds. + func synchronize(update: @escaping (TimeInterval) -> Void) +} + +internal class DatadogNTPDateProvider: ServerDateProvider { + let kronos: KronosClockProtocol + + init(kronos: KronosClockProtocol = KronosClock()) { + self.kronos = kronos + } + + func synchronize(update: @escaping (TimeInterval) -> Void) { + kronos.sync( + from: DatadogNTPServers.randomElement()!, // swiftlint:disable:this force_unwrapping + first: { _, offset in + update(offset) + }, + completion: { now, offset in + // Kronos only notifies for the first and last samples. + // In case, the last sample does not return an offset, we calculate the offset + // from the returned `now` parameter. The `now` parameter in this callback + // is `Clock.now` and it can be either offset computed from prior samples or persisted + // in user defaults from previous app session. + if let offset = offset ?? now?.timeIntervalSinceNow { + update(offset) + + let difference = (offset * 1_000).rounded() / 1_000 + DD.logger.debug( + """ + NTP time synchronization completed. + Server time will be used for signing events (\(difference)s difference with device time). + """ + ) + } else { + update(0) + + DD.logger.error( + """ + NTP time synchronization failed. + Device time will be used for signing events. + """ + ) + } + } + ) + + // `Kronos.sync` first loads the previous state from the `UserDefaults` if any. + // We can invoke `Clock.now` to retrieve the stored offset. + if let offset = kronos.now?.timeIntervalSinceNow { + update(offset) + } + } +} + +/// The Server Offset Publisher provides updates on time offset between the +/// local time and one of the Datadog's NTP pool. +/// +/// This publisher uses a modified version of the ``MobileNativeFoundation/Kronos`` +/// see. https://github.com/MobileNativeFoundation/Kronos +/// +/// The ``KronosClockPublisher/publish`` will start syncing with one of the pool +/// picked randomly from ``DatadogNTPServers``. +/// +/// The time offset is defined in seconds. +internal final class ServerOffsetPublisher: ContextValuePublisher { + /// The initial offset is 0. + let initialValue: TimeInterval = .zero + + private var provider: ServerDateProvider? + + /// Creates a publisher using the given `KronosClock` implementation. + /// + /// - Parameter kronos: An object complying with `KronosClockProtocol`. + init(provider: ServerDateProvider = DatadogNTPDateProvider()) { + self.provider = provider + } + + func publish(to receiver: @escaping ContextValueReceiver) { + provider?.synchronize(update: receiver) + } + + func cancel() { + provider = nil + } +} diff --git a/DatadogCore/Sources/Core/Context/TrackingConsentPublisher.swift b/DatadogCore/Sources/Core/Context/TrackingConsentPublisher.swift new file mode 100644 index 0000000000..3ce054de2f --- /dev/null +++ b/DatadogCore/Sources/Core/Context/TrackingConsentPublisher.swift @@ -0,0 +1,32 @@ +/* + * 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 + +/// Publishes the user consent to receiver. +internal final class TrackingConsentPublisher: ContextValuePublisher { + let initialValue: TrackingConsent + + private var receiver: ContextValueReceiver? + + var consent: TrackingConsent { + didSet { receiver?(consent) } + } + + init(consent: TrackingConsent) { + self.initialValue = consent + self.consent = consent + } + + func publish(to receiver: @escaping ContextValueReceiver) { + self.receiver = receiver + } + + func cancel() { + receiver = nil + } +} diff --git a/DatadogCore/Sources/Core/Context/UserInfoPublisher.swift b/DatadogCore/Sources/Core/Context/UserInfoPublisher.swift new file mode 100644 index 0000000000..3edeadd5bc --- /dev/null +++ b/DatadogCore/Sources/Core/Context/UserInfoPublisher.swift @@ -0,0 +1,27 @@ +/* + * 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 + +/// Publishes the current `UserInfo` value to receiver. +internal final class UserInfoPublisher: ContextValuePublisher { + let initialValue: UserInfo? = .empty + + private var receiver: ContextValueReceiver? + + var current: UserInfo = .empty { + didSet { receiver?(current) } + } + + func publish(to receiver: @escaping ContextValueReceiver) { + self.receiver = receiver + } + + func cancel() { + receiver = nil + } +} diff --git a/DatadogCore/Sources/Core/DataStore/DataStore+TLV.swift b/DatadogCore/Sources/Core/DataStore/DataStore+TLV.swift new file mode 100644 index 0000000000..be8be90e36 --- /dev/null +++ b/DatadogCore/Sources/Core/DataStore/DataStore+TLV.swift @@ -0,0 +1,21 @@ +/* + * 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 + +/// Represents the type of TLV block used in Data Store files. +internal enum DataStoreBlockType: UInt16 { + /// The version of data format in `data` block. + case version = 0x00 + /// The actual data stored in the file. + case data = 0x01 +} + +/// Represents a TLV data block stored in data store files. +internal typealias DataStoreBlock = TLVBlock + +/// Represents a TLV reader for data store files. +internal typealias DataStoreBlockReader = TLVBlockReader diff --git a/DatadogCore/Sources/Core/DataStore/DataStoreFileReader.swift b/DatadogCore/Sources/Core/DataStore/DataStoreFileReader.swift new file mode 100644 index 0000000000..6898792ca2 --- /dev/null +++ b/DatadogCore/Sources/Core/DataStore/DataStoreFileReader.swift @@ -0,0 +1,56 @@ +/* + * 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 + +internal enum DataStoreFileReadingError: Error { + /// Indicates unexpected TLV blocks encountered during file reading. + case unexpectedBlocks([DataStoreBlockType]) + /// Indicates unexpected order of TLV blocks encountered during file reading. + case unexpectedBlocksOrder([DataStoreBlockType]) + /// Indicates that the data size is less than the size of the desired type to read from the value of a TLV block. + case insufficientVersionBytes +} + +internal struct DataStoreFileReader { + internal enum Constants { + /// The maximum length of value block. + static let maxBlockLength = DataStoreFileWriter.Constants.maxDataLength + } + + let file: File + + func read() throws -> (Data, DataStoreKeyVersion) { + let reader = DataStoreBlockReader( + input: try file.stream(), + maxBlockLength: Constants.maxBlockLength + ) + let blocks = try reader.all() + + guard blocks.count == 2 else { + throw DataStoreFileReadingError.unexpectedBlocks(blocks.map { $0.type }) + } + guard blocks[0].type == .version, blocks[1].type == .data else { + throw DataStoreFileReadingError.unexpectedBlocksOrder(blocks.map { $0.type }) + } + + let version: DataStoreKeyVersion = try value(from: blocks[0].data) + let data = blocks[1].data + + return (data, version) + } + + // MARK: - Decoding + + private func value(from data: Data) throws -> T { + guard data.count >= MemoryLayout.size else { + throw DataStoreFileReadingError.insufficientVersionBytes + } + + return data.withUnsafeBytes { $0.load(as: T.self) } + } +} diff --git a/DatadogCore/Sources/Core/DataStore/DataStoreFileWriter.swift b/DatadogCore/Sources/Core/DataStore/DataStoreFileWriter.swift new file mode 100644 index 0000000000..60c180267e --- /dev/null +++ b/DatadogCore/Sources/Core/DataStore/DataStoreFileWriter.swift @@ -0,0 +1,46 @@ +/* + * 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 + +internal enum DataStoreFileWritingError: Error { + case failedToEncodeVersion(Error) + case failedToEncodeData(Error) +} + +internal struct DataStoreFileWriter { + internal enum Constants { + /// The maximum length of data (Value) in TLV block defining key data. + static let maxDataLength = 10.MB.asUInt64() // 10MB + } + + let file: File + + func write(data: Data, version: DataStoreKeyVersion) throws { + let versionBlock = DataStoreBlock(type: .version, data: self.data(from: version)) + let dataBlock = DataStoreBlock(type: .data, data: data) + + var encoded = Data() + do { + try encoded.append(versionBlock.serialize(maxLength: UInt64(MemoryLayout.size))) + } catch let error { + throw DataStoreFileWritingError.failedToEncodeVersion(error) + } + do { + try encoded.append(dataBlock.serialize(maxLength: Constants.maxDataLength)) + } catch let error { + throw DataStoreFileWritingError.failedToEncodeData(error) + } + try file.write(data: encoded) // atomic write + } + + // MARK: - Encoding + + private func data(from value: T) -> Data { + return withUnsafeBytes(of: value) { Data($0) } + } +} diff --git a/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift new file mode 100644 index 0000000000..750dda7166 --- /dev/null +++ b/DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift @@ -0,0 +1,152 @@ +/* + * 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 concrete implementation of the `DataStore` protocol using file storage. +internal final class FeatureDataStore: DataStore { + 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 + } + + /// The name of the feature this instance of data store operates on. + private let feature: String + /// The directory specific to the instance of SDK that holds this feature. + private let coreDirectory: CoreDirectory + /// The data store directory path specific to the `feature`. + /// It is relative path inside `coreDirectory`. + internal let directoryPath: String + /// The queue for managing data store operations. + private let queue: DispatchQueue + /// The telemetry endpoint for sending data store errors. + private let telemetry: Telemetry + + init( + feature: String, + directory: CoreDirectory, + queue: DispatchQueue, + telemetry: Telemetry + ) { + self.feature = feature + self.coreDirectory = directory + self.directoryPath = coreDirectory.getDataStorePath(forFeatureNamed: feature) + self.queue = queue + self.telemetry = telemetry + } + + func setValue(_ value: Data, forKey key: String, version: DataStoreKeyVersion) { + queue.async { [weak self] in + guard let self = self else { + return + } + + do { + try self.write(data: value, forKey: key, version: version) + } catch let error { + DD.logger.error("[Data Store] Error on setting `\(key)` value for `\(self.feature)`", error: error) + self.telemetry.error("[Data Store] Error on setting `\(key)` value for `\(self.feature)`", error: DDError(error: error)) + } + } + } + + func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) { + queue.async { [weak self] in + guard let self = self else { + return + } + + do { + let result = try self.readData(forKey: key) + callback(result) + } catch let error { + callback(.error(error)) + DD.logger.error("[Data Store] Error on getting `\(key)` value for `\(self.feature)`", error: error) + self.telemetry.error("[Data Store] Error on getting `\(key)` value for `\(self.feature)`", error: DDError(error: error)) + } + } + } + + func removeValue(forKey key: String) { + queue.async { [weak self] in + guard let self = self else { + return + } + + do { + try self.deleteData(forKey: key) + } catch let error { + DD.logger.error("[Data Store] Error on deleting `\(key)` value for `\(self.feature)`", error: error) + self.telemetry.error("[Data Store] Error on deleting `\(key)` value for `\(self.feature)`", error: DDError(error: error)) + } + } + } + + 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 { + // Get or create storage directory. We call it each time, to take into account that + // the parent `cache/` location might be erased by the OS at any moment. + let directory = try coreDirectory.coreDirectory.createSubdirectory(path: directoryPath) + + let file: File + if directory.hasFile(named: key) { + file = try directory.file(named: key) + } else { + file = try directory.createFile(named: key) + } + + let writer = DataStoreFileWriter(file: file) + try writer.write(data: data, version: version) + } + + private func readData(forKey key: String) throws -> DataStoreValueResult { + // Get storage directory if it exists. + guard let directory = try? coreDirectory.coreDirectory.subdirectory(path: directoryPath) else { + return .noValue + } + + guard directory.hasFile(named: key) else { + return .noValue + } + + let file = try directory.file(named: key) + let reader = DataStoreFileReader(file: file) + let (data, version) = try reader.read() + return .value(data, version) + } + + private func deleteData(forKey key: String) throws { + // Get storage directory if it exists. + guard let directory = try? coreDirectory.coreDirectory.subdirectory(path: directoryPath) else { + return + } + + if directory.hasFile(named: key) { + try directory.file(named: key).delete() + } + } +} + +extension FeatureDataStore: Flushable { + func flush() { + queue.sync {} + } +} diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift new file mode 100644 index 0000000000..189f7280b4 --- /dev/null +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -0,0 +1,528 @@ +/* + * 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 + +/// Core implementation of Datadog SDK. +/// +/// The core provides a storage and upload mechanism for each registered Feature +/// based on their respective configuration. +/// +/// By complying with `DatadogCoreProtocol`, the core can +/// provide context and writing scopes to Features for event recording. +internal final class DatadogCore { + /// The root location for storing Features data in this instance of the SDK. + /// For each Feature a set of subdirectories is created inside `CoreDirectory` based on their storage configuration. + let directory: CoreDirectory + + /// The storage r/w GDC queue. + let readWriteQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-read-write", + autoreleaseFrequency: .workItem, + target: .global(qos: .utility) + ) + + /// The system date provider. + let dateProvider: DateProvider + + /// The user consent publisher. + let consentPublisher: TrackingConsentPublisher + + /// The core SDK performance presets. + let performance: PerformancePreset + + /// The HTTP Client for uploads. + let httpClient: HTTPClient + + /// The on-disk data encryption. + let encryption: DataEncryption? + + /// The user info publisher that publishes value to the + /// `contextProvider` + let userInfoPublisher = UserInfoPublisher() + + /// The application version publisher. + let applicationVersionPublisher: ApplicationVersionPublisher + + /// The message-bus instance. + let bus = MessageBus() + + /// Registry for Features. + @ReadWriteLock + private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:] + + /// Registry for Features. + @ReadWriteLock + private var features: [String: DatadogFeature] = [:] + + /// The core context provider. + internal let contextProvider: DatadogContextProvider + + /// Flag defining if background tasks are enabled. + internal let backgroundTasksEnabled: Bool + + /// Flag defining if the SDK is run from an extension. + internal let isRunFromExtension: Bool + + /// Maximum number of batches per upload. + internal let maxBatchesPerUpload: Int + + /// Creates a core instance. + /// + /// - Parameters: + /// - directory: The core directory for this instance of the SDK. + /// - dateProvider: The system date provider. + /// - initialConsent: The initial user consent. + /// - performance: The core SDK performance presets. + /// - httpClient: The HTTP Client for uploads. + /// - encryption: The on-disk data encryption. + /// - contextProvider: The core context provider. + /// - applicationVersion: The application version. + init( + directory: CoreDirectory, + dateProvider: DateProvider, + initialConsent: TrackingConsent, + performance: PerformancePreset, + httpClient: HTTPClient, + encryption: DataEncryption?, + contextProvider: DatadogContextProvider, + applicationVersion: String, + maxBatchesPerUpload: Int, + backgroundTasksEnabled: Bool, + isRunFromExtension: Bool = false + ) { + self.directory = directory + self.dateProvider = dateProvider + self.performance = performance + self.httpClient = httpClient + self.encryption = encryption + self.contextProvider = contextProvider + self.maxBatchesPerUpload = maxBatchesPerUpload + self.backgroundTasksEnabled = backgroundTasksEnabled + self.isRunFromExtension = isRunFromExtension + self.applicationVersionPublisher = ApplicationVersionPublisher(version: applicationVersion) + self.consentPublisher = TrackingConsentPublisher(consent: initialConsent) + self.contextProvider.subscribe(\.userInfo, to: userInfoPublisher) + self.contextProvider.subscribe(\.version, to: applicationVersionPublisher) + self.contextProvider.subscribe(\.trackingConsent, to: consentPublisher) + + // connect the core to the message bus. + // the bus will keep a weak ref to the core. + bus.connect(core: self) + + // forward any context change on the message-bus + self.contextProvider.publish { [weak self] context in + self?.send(message: .context(context)) + } + } + + /// 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 + /// - email: User's email, if any + /// - extraInfo: User's custom attributes, if any + func setUserInfo( + id: String? = nil, + name: String? = nil, + email: String? = nil, + extraInfo: [AttributeKey: AttributeValue] = [:] + ) { + let userInfo = UserInfo( + id: id, + name: name, + email: email, + extraInfo: extraInfo + ) + + userInfoPublisher.current = userInfo + } + + /// Add or override the extra info of the current user + /// + /// - Parameters: + /// - extraInfo: The user's custom attibutes to add or override + func addUserExtraInfo(_ newExtraInfo: [AttributeKey: AttributeValue?]) { + var extraInfo = userInfoPublisher.current.extraInfo + newExtraInfo.forEach { extraInfo[$0.key] = $0.value } + userInfoPublisher.current.extraInfo = extraInfo + } + + /// 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 { + 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 + } + } + + /// 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. + /// + /// After being added to the bus, the core will send the current context to receiver. + /// + /// - Parameters: + /// - messageReceiver: The new message receiver. + /// - key: The key associated with the receiver. + private func add(messageReceiver: FeatureMessageReceiver, forKey key: String) { + bus.connect(messageReceiver, forKey: key) + contextProvider.read { context in + self.bus.queue.async { messageReceiver.receive(message: .context(context), from: self) } + } + } + + /// A list of storage units of currently registered Features. + private var allStorages: [FeatureStorage] { + stores.values.map { $0.storage } + } + + /// A list of upload units of currently registered Features. + private var allUploads: [FeatureUpload] { + 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**. + /// + /// Upon return, it is safe to assume that all events were stored and got uploaded. The SDK was deinitialised so this instance of core is missfunctional. + func flushAndTearDown() { + flush() + + // At this point we can assume that all write operations completed and resulted with writing events to + // storage. We now temporarily authorize storage for making all files readable ("uploadable") and perform + // arbitrary uploads (without retrying on failure). + allStorages.forEach { $0.setIgnoreFilesAgeWhenReading(to: true) } + allUploads.forEach { $0.flushAndTearDown() } + allStorages.forEach { $0.setIgnoreFilesAgeWhenReading(to: false) } + + stop() + } + + /// Stops all processes for this instance of the Datadog core by + /// deallocating all Features and their storage & upload units. + func stop() { + stores = [:] + features = [:] + } +} + +extension DatadogCore: DatadogCoreProtocol { + /// Registers a Feature instance. + /// + /// A Feature collects and transfers data to a Datadog Product (e.g. Logs, RUM, ...). A registered Feature can + /// open a `FeatureScope` to write events, the core will then be responsible for storing and uploading events + /// in a efficient manner. Performance presets for storage and upload are define when instanciating the core instance. + /// + /// A Feature can also communicate to other Features by sending message on the bus that is managed by the core. + /// + /// - Parameter feature: The Feature instance. + func register(feature: T) throws where T: DatadogFeature { + if let feature = feature as? DatadogRemoteFeature { + let featureDirectories = try directory.getFeatureDirectories(forFeatureNamed: T.name) + + let performancePreset: PerformancePreset + if let override = feature.performanceOverride { + performancePreset = performance.updated(with: override) + } else { + performancePreset = performance + } + + let storage = FeatureStorage( + featureName: T.name, + queue: readWriteQueue, + directories: featureDirectories, + dateProvider: dateProvider, + performance: performancePreset, + encryption: encryption, + backgroundTasksEnabled: backgroundTasksEnabled, + telemetry: telemetry + ) + + let upload = FeatureUpload( + featureName: T.name, + contextProvider: contextProvider, + fileReader: storage.reader, + requestBuilder: feature.requestBuilder, + httpClient: httpClient, + performance: performancePreset, + backgroundTasksEnabled: backgroundTasksEnabled, + maxBatchesPerUpload: maxBatchesPerUpload, + isRunFromExtension: isRunFromExtension, + telemetry: telemetry + ) + + stores[T.name] = ( + storage: storage, + upload: upload + ) + + // If there is any persisted data recorded with `.pending` consent, + // it should be deleted on Feature startup: + storage.clearUnauthorizedData() + } + + features[T.name] = feature + add(messageReceiver: feature.messageReceiver, forKey: T.name) + } + + /// Retrieves a Feature by its name and type. + /// + /// A Feature type can be specified as parameter or inferred from the return type: + /// + /// let feature = core.feature(named: "foo", type: Foo.self) + /// let feature: Foo? = core.feature(named: "foo") + /// + /// - Parameters: + /// - name: The Feature's name. + /// - type: The Feature instance type. + /// - Returns: The Feature if any. + func feature(named name: String, type: T.Type) -> T? { + features[name] as? T + } + + func scope(for featureType: Feature.Type) -> FeatureScope where Feature: DatadogFeature { + return CoreFeatureScope(in: self) + } + + func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { + contextProvider.write { $0.baggages[key] = baggage() } + } + + func send(message: FeatureMessage, else fallback: @escaping () -> Void) { + bus.send(message: message, else: fallback) + } +} + +internal class CoreFeatureScope: @unchecked Sendable, FeatureScope where Feature: DatadogFeature { + private weak var core: DatadogCore? + private let store: FeatureDataStore + + init(in core: DatadogCore) { + self.core = core + self.store = FeatureDataStore( + feature: Feature.name, + directory: core.directory, + queue: core.readWriteQueue, + telemetry: core.telemetry + ) + } + + func eventWriteContext(bypassConsent: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { + guard let core = core else { + return // core is deinitialized + } + // Capture the storage reference so it is available until async block completion. This is to ensure + // that we write events which were collected on the caller thread even if the core was released in the meantime. + guard let storage = core.stores[Feature.name]?.storage else { + if core.get(feature: Feature.self) != nil { // the feature is running, but has no storage + DD.logger.error( + "Failed to obtain Event Write Context for '\(Feature.name)' because it is not a `DatadogRemoteFeature`." + ) + #if DEBUG + assertionFailure("Obtaining Event Write Context for '\(Feature.name)' but it is not a `DatadogRemoteFeature`.") + #endif + } + return + } + + // (on user thread) request SDK context + context { context in + // (on context thread) call the block + let writer = storage.writer(for: bypassConsent ? .granted : context.trackingConsent) + block(context, writer) + } + } + + func context(_ block: @escaping (DatadogContext) -> Void) { + // (on user thread) request SDK context + core?.contextProvider.read { context in + // (on context thread) call the block + block(context) + } + } + + var dataStore: DataStore { + return (core != nil) ? store : NOPDataStore() // only available when the core exists + } + + func send(message: FeatureMessage, else fallback: @escaping () -> Void) { + core?.send(message: message, else: fallback) + } + + func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { + core?.set(baggage: baggage, forKey: key) + } + + var telemetry: Telemetry { + return core?.telemetry ?? NOPTelemetry() + } +} + +extension DatadogContextProvider { + /// Creates a core context provider with the given configuration, + convenience init( + site: DatadogSite, + clientToken: String, + service: String, + env: String, + version: String, + buildNumber: String, + buildId: String?, + variant: String?, + source: String, + nativeSourceOverride: String?, + sdkVersion: String, + ciAppOrigin: String?, + applicationName: String, + applicationBundleIdentifier: String, + applicationBundleType: BundleType, + applicationVersion: String, + sdkInitDate: Date, + device: DeviceInfo, + processInfo: ProcessInfo, + dateProvider: DateProvider, + serverDateProvider: ServerDateProvider, + notificationCenter: NotificationCenter, + appStateProvider: AppStateProvider + ) { + let context = DatadogContext( + site: site, + clientToken: clientToken, + service: service, + env: env, + version: applicationVersion, + buildNumber: buildNumber, + buildId: buildId, + variant: variant, + source: source, + sdkVersion: sdkVersion, + ciAppOrigin: ciAppOrigin, + applicationName: applicationName, + applicationBundleIdentifier: applicationBundleIdentifier, + applicationBundleType: applicationBundleType, + sdkInitDate: dateProvider.now, + device: device, + nativeSourceOverride: nativeSourceOverride, + // this is a placeholder waiting for the `ApplicationStatePublisher` + // to be initialized on the main thread, this value will be overrided + // as soon as the subscription is made. + applicationStateHistory: .active(since: dateProvider.now) + ) + + self.init(context: context) + + subscribe(\.serverTimeOffset, to: ServerOffsetPublisher(provider: serverDateProvider)) + + #if !os(macOS) + subscribe(\.launchTime, to: LaunchTimePublisher()) + #endif + + subscribe(\.networkConnectionInfo, to: NWPathMonitorPublisher()) + + #if os(iOS) && !targetEnvironment(macCatalyst) && !(swift(>=5.9) && os(visionOS)) + subscribe(\.carrierInfo, to: CarrierInfoPublisher()) + #endif + + #if os(iOS) && !targetEnvironment(simulator) + 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( + appStateProvider: appStateProvider, + notificationCenter: notificationCenter, + dateProvider: dateProvider + ) + self.subscribe(\.applicationStateHistory, to: applicationStatePublisher) + } + #endif + } +} + +extension DatadogCore: Flushable { + /// Flushes asynchronous operations related to events write, context and message bus propagation in this instance of the SDK + /// with **blocking the caller thread** till their completion. + /// + /// Upon return, it is safe to assume that all events are stored. No assumption on their upload should be made - to force events upload + /// use `flushAndTearDown()` instead. + func flush() { + // 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 + // on these queues are also awaited. Effectively, this is no different than short-time sleep() on current + // thread and it has the same drawbacks (including: it might become flaky). Until we find a better solution + // this is enough to get consistency in tests - but won't be reliable in any public "deinitialize" API. + for _ in 0..<5 { + // First, flush bus queue - because messages can lead to obtaining "event write context" (reading + // context & performing write) in other Features: + bus.flush() + + // Next, flush flushable Features - finish current data collection to open "event write contexts": + features.forEach { $0.flush() } + + // Next, flush context queue - because it indicates the entry point to "event write context" and + // actual writes dispatched from it: + contextProvider.flush() + + // Last, flush read-write queue - it always comes last, no matter if the write operation is dispatched + // from "event write context" started on user thread OR if it happens upon receiving an "event" message + // in other Feature: + readWriteQueue.sync { } + } + } +} + +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 new file mode 100644 index 0000000000..c264236fb6 --- /dev/null +++ b/DatadogCore/Sources/Core/MessageBus.swift @@ -0,0 +1,135 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// The message-bus sends messages to a set of registered receivers. +/// +/// The bus dispatches messages on a serial queue. +internal final class MessageBus { + /// The message bus GDC queue. + let queue = DispatchQueue( + label: "com.datadoghq.ios-sdk-message-bus", + target: .global(qos: .utility) + ) + + /// A weak core reference. + /// + /// The core **must** be accessed within the queue. + private weak var core: DatadogCoreProtocol? + + /// The message bus used to dispatch messages to registered features. + /// + /// The bus **must** be accessed within the queue. + private var bus: [String: FeatureMessageReceiver] = [:] + + /// The current configuration. + /// + /// The message-bus wil accumulate configuration by merge. A message + /// with the configuration will be dispatched once after a specified delay, + /// 5 seconds by default. + /// + /// The configuration **must** be accessed within the queue. + private var configuration: ConfigurationTelemetry? + + /// Creates a bus for the given core. + /// + /// The message-bus keeps a weak reference to the core. + /// - Parameter configurationDispatchTime: The delay to dispatch the + /// configuration telemetry + init(configurationDispatchTime: DispatchTimeInterval = .seconds(5)) { + queue.asyncAfter(deadline: .now() + configurationDispatchTime) { + guard let core = self.core, let configuration = self.configuration else { + return + } + + self.bus.values.forEach { + $0.receive(message: .telemetry(.configuration(configuration)), from: core) + } + } + } + + /// Connects the core to the bus. + /// + /// The message-bus keeps a weak reference to the core. + /// + /// - Parameter core: The core ference. + func connect(core: DatadogCoreProtocol) { + queue.async { self.core = core } + } + + /// Connects a receiver with a given key. + /// + /// - Parameters: + /// - receiver: The message receiver. + /// - key: The key associated with the receiver. + func connect(_ receiver: FeatureMessageReceiver, forKey key: String) { + queue.async { self.bus[key] = receiver } + } + + /// 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) } + } + + /// Sends a message to receivers registered in this bus. + /// + /// If the message could not be processed by any registered feature, the fallback closure + /// will be invoked. Do not make any assumption on which thread the fallback is called. + /// + /// - Parameters: + /// - message: The message. + /// - fallback: The fallback closure to call when the message could not be + /// processed by any Features on the bus. + func send(message: FeatureMessage, else fallback: @escaping () -> Void = {}) { + if // Configuration Telemetry Message + case .telemetry(let telemetry) = message, + case .configuration(let configuration) = telemetry { + return save(configuration: configuration) + } + + queue.async { + guard let core = self.core else { + return + } + + let receivers = self.bus.values.filter { + $0.receive(message: message, from: core) + } + + if receivers.isEmpty { + fallback() + } + } + } + + /// Saves to current Configuration Telemetry. + /// + /// The configuration can be partial, the bus supports accumulation of + /// configuration for lazy initialization of the SDK. + /// + /// - Parameter configuration: The SDK configuration. + private func save(configuration: ConfigurationTelemetry) { + queue.async { + // merge with the current configuration if any + self.configuration = self.configuration.map { + $0.merged(with: configuration) + } ?? configuration + } + } +} + +extension MessageBus: Flushable { + /// Awaits completion of all asynchronous operations. + /// + /// **blocks the caller thread** + func flush() { + queue.sync { } + } +} diff --git a/DatadogCore/Sources/Core/Storage/DataEncryption.swift b/DatadogCore/Sources/Core/Storage/DataEncryption.swift new file mode 100644 index 0000000000..6d12e4d03b --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/DataEncryption.swift @@ -0,0 +1,29 @@ +/* + * 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 + +/// Interface that allows storing data in encrypted format. Encryption/decryption round should +/// return exactly the same data as it given for the encryption originally (even if decryption +/// happens in another process/app launch). +public protocol DataEncryption { + /// Encrypts given `Data` with user-chosen encryption. + /// + /// - Parameter data: Data to encrypt. + /// - Returns: The encrypted data. + func encrypt(data: Data) throws -> Data + + /// Decrypts given `Data` with user-chosen encryption. + /// + /// Beware that data to decrypt could be encrypted in a previous app launch, so + /// implementation should be aware of the case when decryption could fail (for example, + /// key used for encryption is different from key used for decryption, if they are unique + /// for every app launch). + /// + /// - Parameter data: Data to decrypt. + /// - Returns: The decrypted data. + func decrypt(data: Data) throws -> Data +} diff --git a/DatadogCore/Sources/Core/Storage/Directories.swift b/DatadogCore/Sources/Core/Storage/Directories.swift new file mode 100644 index 0000000000..6954f189c4 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Directories.swift @@ -0,0 +1,78 @@ +/* + * 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 + +/// Indicates the main directory for a given instance of the SDK. +/// Each instance of `DatadogCore` creates its own `CoreDirectory` to manage data for registered Features. +/// The core directory is created under `/Library/Caches` and uses a name that identifies the certain instance +/// of the SDK (``): +/// +/// ``` +/// /Library/Cache/com.datadoghq/v2// +/// ``` +/// +/// Note: System may delete data in `/Library/Cache` to free up disk space which reduces the impact on devices working +/// under heavy space pressure. This is intentional for Datadog SDK to have its data purged when system needs more memory +/// for other apps. +internal struct CoreDirectory { + /// A known OS location the core directory is created within:`/Library/Cache`. + let osDirectory: Directory + /// The core directory specific to this instance of the SDK: `/Library/Cache/com.datadoghq/v2/`. + let coreDirectory: Directory + + /// Obtains subdirectories for managing batch files for given Feature (creates if don't exist). + /// + /// - Parameter name: The given Feature name. + /// - Returns: The Feature's directories + func getFeatureDirectories(forFeatureNamed name: String) throws -> FeatureDirectories { + return FeatureDirectories( + unauthorized: try coreDirectory.createSubdirectory(path: "\(name)/intermediate-v2"), + 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. + /// - site: The cor instance site. + init(in osDirectory: Directory, instanceName: String, site: DatadogSite) throws { + let sdkInstanceUUID = sha256("\(instanceName)\(site)") + let path = "com.datadoghq/v2/\(sdkInstanceUUID)" + + self.init( + osDirectory: osDirectory, + coreDirectory: try osDirectory.createSubdirectory(path: path) + ) + } +} + +/// Bundles directories for managing data in single Feature. +internal struct FeatureDirectories { + /// Data directory for storing unauthorized data collected without knowing the tracking consent value. + /// Due to the consent change, data in this directory may be either moved to `authorized` folder or entirely deleted. + let unauthorized: Directory + /// Data directory for storing authorized data collected when tracking consent is granted. + /// Consent change does not impact data already stored in this folder. + /// Data in this folder gets uploaded to the server. + let authorized: Directory +} diff --git a/DatadogCore/Sources/Core/Storage/EventGenerator.swift b/DatadogCore/Sources/Core/Storage/EventGenerator.swift new file mode 100644 index 0000000000..0813b6784d --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/EventGenerator.swift @@ -0,0 +1,60 @@ +/* + * 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 + +/// Event generator that generates events from the given data blocks. +internal struct EventGenerator: Sequence, IteratorProtocol { + private let dataBlocks: [BatchDataBlock] + private var index: Int + + init(dataBlocks: [BatchDataBlock], index: Int = 0) { + self.dataBlocks = dataBlocks + self.index = index + } + + /// Returns the next event. + /// + /// Data format + /// ``` + /// [EVENT 1 METADATA] [EVENT 1] [EVENT 2 METADATA] [EVENT 2] [EVENT 3] + /// ``` + /// + /// - Returns: The next event or `nil` if there are no more events. + /// - Note: a `DataBlock` with `.event` type marks the beginning of the event. + /// It is either followed by another `DataBlock` with `.event` type or + /// by a `DataBlock` with `.metadata` type. + mutating func next() -> Event? { + guard index < dataBlocks.count else { + return nil + } + + var metadata: BatchDataBlock? = nil + // If the next block is an event metadata, read it. + if dataBlocks[index].type == .eventMetadata { + metadata = dataBlocks[index] + index += 1 + } + + // If this is the last block, return nil. + // there cannot be a metadata block without an event block. + guard index < dataBlocks.count else { + return nil + } + + // If the next block is an event, read it. + guard dataBlocks[index].type == .event else { + // this is safeguard against corrupted data. + // if there was a metadata block, it will be skipped. + return next() + } + let event = dataBlocks[index] + index += 1 + + return Event(data: event.data, metadata: metadata?.data) + } +} diff --git a/DatadogCore/Sources/Core/Storage/FeatureStorage.swift b/DatadogCore/Sources/Core/Storage/FeatureStorage.swift new file mode 100644 index 0000000000..31eb3f13d5 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/FeatureStorage.swift @@ -0,0 +1,167 @@ +/* + * 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 + +internal struct FeatureStorage { + /// The name of this Feature, used to distinguish storage instances in telemetry and logs. + let featureName: String + /// Queue for performing all I/O operations (writes, reads and files management). + let queue: DispatchQueue + /// Directories for managing data in this Feature. + let directories: FeatureDirectories + /// Orchestrates files collected in `.granted` consent. + let authorizedFilesOrchestrator: FilesOrchestratorType + /// Orchestrates files collected in `.pending` consent. + let unauthorizedFilesOrchestrator: FilesOrchestratorType + /// Encryption algorithm applied to persisted data. + let encryption: DataEncryption? + /// Telemetry interface. + let telemetry: Telemetry + + func writer(for trackingConsent: TrackingConsent) -> Writer { + switch trackingConsent { + case .granted: + return AsyncWriter( + execute: FileWriter( + orchestrator: authorizedFilesOrchestrator, + encryption: encryption, + telemetry: telemetry + ), + on: queue + ) + case .notGranted: + return NOPWriter() + case .pending: + return AsyncWriter( + execute: FileWriter( + orchestrator: unauthorizedFilesOrchestrator, + encryption: encryption, + telemetry: telemetry + ), + on: queue + ) + } + } + + var reader: Reader { + DataReader( + readWriteQueue: queue, + fileReader: FileReader( + orchestrator: authorizedFilesOrchestrator, + encryption: encryption, + telemetry: telemetry + ) + ) + } + + func migrateUnauthorizedData(toConsent consent: TrackingConsent) { + queue.async { + do { + switch consent { + case .notGranted: + try directories.unauthorized.deleteAllFiles() + case .granted: + try directories.unauthorized.moveAllFiles(to: directories.authorized) + case .pending: + break + } + } catch { + telemetry.error( + "Failed to migrate unauthorized data in \(featureName) after consent change to to \(consent)", + error: error + ) + } + } + } + + func clearUnauthorizedData() { + queue.async { + do { + try directories.unauthorized.deleteAllFiles() + } catch { + telemetry.error("Failed clear unauthorized data in \(featureName)", error: error) + } + } + } + + func clearAllData() { + queue.async { + do { + try directories.unauthorized.deleteAllFiles() + try directories.authorized.deleteAllFiles() + } catch { + telemetry.error("Failed clear all data in \(featureName)", error: error) + } + } + } + + func setIgnoreFilesAgeWhenReading(to value: Bool) { + queue.sync { + authorizedFilesOrchestrator.ignoreFilesAgeWhenReading = value + unauthorizedFilesOrchestrator.ignoreFilesAgeWhenReading = value + } + } +} + +extension FeatureStorage { + init( + featureName: String, + queue: DispatchQueue, + directories: FeatureDirectories, + dateProvider: DateProvider, + performance: PerformancePreset, + encryption: DataEncryption?, + backgroundTasksEnabled: Bool, + telemetry: Telemetry + ) { + let trackName = BatchMetric.trackValue(for: featureName) + + if trackName == nil { + DD.logger.error("Can't determine track name for feature named '\(featureName)'") + } + + let authorizedFilesOrchestrator = FilesOrchestrator( + directory: directories.authorized, + performance: performance, + dateProvider: dateProvider, + telemetry: telemetry, + metricsData: trackName.map { trackName in + return FilesOrchestrator.MetricsData( + trackName: trackName, + consentLabel: BatchMetric.consentGrantedValue, + uploaderPerformance: performance, + backgroundTasksEnabled: backgroundTasksEnabled + ) + } + ) + let unauthorizedFilesOrchestrator = FilesOrchestrator( + directory: directories.unauthorized, + performance: performance, + dateProvider: dateProvider, + telemetry: telemetry, + metricsData: trackName.map { trackName in + return FilesOrchestrator.MetricsData( + trackName: trackName, + consentLabel: BatchMetric.consentPendingValue, + uploaderPerformance: performance, + backgroundTasksEnabled: backgroundTasksEnabled + ) + } + ) + + self.init( + featureName: featureName, + queue: queue, + directories: directories, + authorizedFilesOrchestrator: authorizedFilesOrchestrator, + unauthorizedFilesOrchestrator: unauthorizedFilesOrchestrator, + encryption: encryption, + telemetry: telemetry + ) + } +} diff --git a/DatadogCore/Sources/Core/Storage/Files/Directory.swift b/DatadogCore/Sources/Core/Storage/Files/Directory.swift new file mode 100644 index 0000000000..dabb9f61f0 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Files/Directory.swift @@ -0,0 +1,176 @@ +/* + * 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 + +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: DirectoryProtocol { + let url: URL + + /// Creates subdirectory with given path under system caches directory. + /// RUMM-2169: Use `Directory.cache().createSubdirectory(path:)` instead. + init(withSubdirectoryPath path: String) throws { + self.init(url: try Directory.cache().createSubdirectory(path: path).url) + } + + init(url: URL) { + 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 { + let subdirectoryURL = url.appendingPathComponent(path, isDirectory: true) + do { + try FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil) + } catch { + throw InternalError(description: "Cannot create subdirectory in `/Library/Caches/` folder.") + } + return Directory(url: subdirectoryURL) + } + + /// Returns directory at given path or throws if it doesn't exist or given `path` is not a directory. + func subdirectory(path: String) throws -> Directory { + let directoryURL = url.appendingPathComponent(path, isDirectory: true) + var isDirectory = ObjCBool(false) + let exists = FileManager.default.fileExists(atPath: directoryURL.path, isDirectory: &isDirectory) + + if exists && isDirectory.boolValue { + return Directory(url: directoryURL) + } else { + throw InternalError(description: "Path doesn't exist or is not a directory: \(directoryURL)") + } + } + + /// Creates file with given name. + func createFile(named fileName: String) throws -> File { + let fileURL = url.appendingPathComponent(fileName, isDirectory: false) + try Data.empty.write(to: fileURL, options: .atomic) + return File(url: fileURL) + } + + /// Checks if a file with given `fileName` exists in this directory. + func hasFile(named fileName: String) -> Bool { + let fileURL = url.appendingPathComponent(fileName, isDirectory: false) + return FileManager.default.fileExists(atPath: fileURL.path) + } + + /// Returns file with given name or throws an error if file does not exist. + func file(named fileName: String) throws -> File { + let fileURL = url.appendingPathComponent(fileName, isDirectory: false) + guard hasFile(named: fileName) else { + throw InternalError(description: "File does not exist at path: \(fileURL.path)") + } + return File(url: fileURL) + } + + /// Returns all files of this directory. + func files() throws -> [File] { + return try FileManager.default + .contentsOfDirectory(at: url, includingPropertiesForKeys: [.isRegularFileKey, .canonicalPathKey]) + .map { url in File(url: url) } + } + + /// Deletes all files in this directory. + func deleteAllFiles() throws { + // Instead of iterating over all files and removing them one by one, we create a temporary + // empty directory and replace source directory content with (empty) temporary folder. + // This makes the deletion atomic, and is more performant in benchmarks. + let temporaryDirectory = try Directory(withSubdirectoryPath: "com.datadoghq/\(UUID().uuidString)") + try retry(times: 3, delay: 0.001) { + _ = try FileManager.default.replaceItemAt(url, withItemAt: temporaryDirectory.url) + } + if FileManager.default.fileExists(atPath: temporaryDirectory.url.path) { + try FileManager.default.removeItem(at: temporaryDirectory.url) + } + } + + /// Moves all files from this directory to `destinationDirectory`. + func moveAllFiles(to destinationDirectory: Directory) throws { + try retry(times: 3, delay: 0.001) { + try files().forEach { file in + let destinationFileURL = destinationDirectory.url.appendingPathComponent(file.name) + try? retry(times: 3, delay: 0.0001) { + try FileManager.default.moveItem(at: file.url, to: destinationFileURL) + } + } + } + } +} + +extension Directory { + /// Returns `Directory` pointing to `/Library/Caches`. + /// - `/Library/Caches` is exclduded from iTunes and iCloud backups by default. + /// - System may delete data in `/Library/Cache` to free up disk space which reduces the impact on devices working under heavy space pressure. + static func cache() throws -> Directory { + guard let cachesDirectoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + throw InternalError(description: "Cannot obtain `/Library/Caches/` url.") + } + return Directory(url: cachesDirectoryURL) + } +} diff --git a/DatadogCore/Sources/Core/Storage/Files/File.swift b/DatadogCore/Sources/Core/Storage/Files/File.swift new file mode 100644 index 0000000000..e4ad03dfda --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Files/File.swift @@ -0,0 +1,130 @@ +/* + * 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 + +/// 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 { + /// Name of this file. + var name: String { get } + + /// Current size of this file. + func size() throws -> UInt64 + + /// Synchronously appends given data at the end of this file. + func append(data: Data) throws +} + +/// Provides convenient interface for reading contents and metadata of the file. +internal protocol ReadableFile { + /// Name of this file. + var name: String { get } + + /// Creates InputStream for reading the available data from this file. + func stream() throws -> InputStream + + /// Deletes this file. + func delete() throws +} + +private enum FileError: Error { + case unableToCreateInputStream +} + +/// 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, FileProtocol, Equatable { + let url: URL + let name: String + + init(url: URL) { + self.url = url + 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) + + // NOTE: RUMM-669 + // https://github.com/DataDog/dd-sdk-ios/issues/214 + // https://en.wikipedia.org/wiki/Xcode#11.x_series + // compiler version needs to have iOS 13.4+ as base SDK + #if compiler(>=5.2) + /** + Even though the `fileHandle.seekToEnd()` should be available since iOS 13.0: + ``` + @available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func seekToEnd() throws -> UInt64 + ``` + it crashes on iOS Simulators prior to iOS 13.4: + ``` + Symbol not found: _$sSo12NSFileHandleC10FoundationE9seekToEnds6UInt64VyKF + ``` + This is fixed in iOS 14/Xcode 12 + */ + if #available(iOS 13.4, tvOS 13.4, *) { + defer { try? fileHandle.close() } + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: data) + } else { + try legacyAppend(data, to: fileHandle) + } + #else + try legacyAppend(data, to: fileHandle) + #endif + } + + func write(data: Data) throws { + // The `.atomic` option writes data to an auxiliary file first and then replace the original + // file with the auxiliary file when the write completes + try data.write(to: url, options: .atomic) + } + + private func legacyAppend(_ data: Data, to fileHandle: FileHandle) throws { + defer { + try? objc_rethrow { + fileHandle.closeFile() + } + } + try objc_rethrow { + fileHandle.seekToEndOfFile() + fileHandle.write(data) + } + } + + func stream() throws -> InputStream { + guard let stream = InputStream(url: url) else { + throw FileError.unableToCreateInputStream + } + return stream + } + + func size() throws -> UInt64 { + let attributes = try FileManager.default.attributesOfItem(atPath: url.path) + return attributes[.size] as? UInt64 ?? 0 + } + + func delete() throws { + try FileManager.default.removeItem(at: url) + } +} diff --git a/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift new file mode 100644 index 0000000000..282a462524 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/FilesOrchestrator.swift @@ -0,0 +1,328 @@ +/* + * 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 + +internal protocol FilesOrchestratorType: AnyObject { + var performance: StoragePerformancePreset { get } + + func getWritableFile(writeSize: UInt64) throws -> WritableFile + func getReadableFiles(excludingFilesNamed excludedFileNames: Set, limit: Int) -> [ReadableFile] + 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. + let dateProvider: DateProvider + /// Performance rules for writing and reading files. + let performance: StoragePerformancePreset + /// Name of the last file returned by `getWritableFile()`. + private var lastWritableFileName: String? = nil + /// Tracks number of times the last file was returned from `getWritableFile(writeSize:)`. + /// This should correspond with number of objects stored in file, assuming that majority of writes succeed (the difference is negligible). + private var lastWritableFileObjectsCount: UInt64 = 0 + /// Tracks the size of last writable file by accumulating the total `writeSize:` requested in `getWritableFile(writeSize:)` + /// This is approximated value as it assumes that all requested writes succed. The actual difference should be negligible. + private var lastWritableFileApproximatedSize: UInt64 = 0 + /// Tracks the last date when writtable file was requested for write operation. + /// It is used to compute the "batch duration" from the moment file was created to the moment it was last written. + private var lastWritableFileLastWriteDate: Date? = nil + /// Telemetry interface. + let telemetry: Telemetry + + /// Extra information for metrics set from this orchestrator. + struct MetricsData { + /// The name of the track reported for this orchestrator. + let trackName: String + /// The label indicating the value of tracking consent that this orchestrator manages files for. + 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, + dateProvider: DateProvider, + telemetry: Telemetry, + metricsData: MetricsData? = nil + ) { + self.directory = directory + self.performance = performance + self.dateProvider = dateProvider + self.telemetry = telemetry + self.metricsData = metricsData + } + + // MARK: - `WritableFile` orchestration + + /// Returns writable file accordingly to default heuristic of creating and reusing files. + /// + /// - Parameter writeSize: the size of data to be written + /// - Returns: `WritableFile` capable of writing data of given size + func getWritableFile(writeSize: UInt64) throws -> WritableFile { + try validate(writeSize: writeSize) + + if let lastWritableFile = reuseLastWritableFileIfPossible(writeSize: writeSize) { // if last writable file can be reused + lastWritableFileObjectsCount += 1 + lastWritableFileApproximatedSize += writeSize + lastWritableFileLastWriteDate = dateProvider.now + return lastWritableFile + } else { + if let closedBatchName = lastWritableFileName { + sendBatchClosedMetric(fileName: closedBatchName) + } + return try createNewWritableFile(writeSize: writeSize) + } + } + + private func validate(writeSize: UInt64) throws { + guard writeSize <= performance.maxObjectSize else { + throw InternalError(description: "data exceeds the maximum size of \(performance.maxObjectSize) bytes.") + } + } + + private func createNewWritableFile(writeSize: UInt64) throws -> WritableFile { + // NOTE: RUMM-610 Because purging files directory is a memory-expensive operation, do it only when a new file + // is created (we assume here that this won't happen too often). In details, this is to avoid over-allocating + // internal `_FileCache` and `_NSFastEnumerationEnumerator` objects in downstream `FileManager` routines. + // This optimisation results with flat allocation graph in a long term (vs endlessly growing if purging + // happens too often). + try purgeFilesDirectoryIfNeeded() + + let newFileName = nextFileName() + let newFile = try directory.createFile(named: newFileName) + lastWritableFileName = newFile.name + lastWritableFileObjectsCount = 1 + lastWritableFileApproximatedSize = writeSize + lastWritableFileLastWriteDate = dateProvider.now + 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) { + return nil // this is expected if the last writable file was deleted + } + + do { + let lastFile = try directory.file(named: lastFileName) + let lastFileCreationDate = fileCreationDateFrom(fileName: lastFile.name) + let lastFileAge = dateProvider.now.timeIntervalSince(lastFileCreationDate) + + let fileIsRecentEnough = lastFileAge <= performance.maxFileAgeForWrite + let fileHasRoomForMore = (try lastFile.size() + writeSize) <= performance.maxFileSize + let fileCanBeUsedMoreTimes = (lastWritableFileObjectsCount + 1) <= performance.maxObjectsInFile + + if fileIsRecentEnough && fileHasRoomForMore && fileCanBeUsedMoreTimes { + return lastFile + } + } catch { + telemetry.error("Failed to reuse last writable file", error: error) + } + } + + return nil + } + + // MARK: - `ReadableFile` orchestration + + func getReadableFiles(excludingFilesNamed excludedFileNames: Set = [], limit: Int = .max) -> [ReadableFile] { + do { + let filesFromOldest = try directory.files() + .map { (file: $0, fileCreationDate: fileCreationDateFrom(fileName: $0.name)) } + .compactMap { try deleteFileIfItsObsolete(file: $0.file, fileCreationDate: $0.fileCreationDate) } + .sorted(by: { $0.fileCreationDate < $1.fileCreationDate }) + + if ignoreFilesAgeWhenReading { + return filesFromOldest + .prefix(limit) + .map { $0.file } + } + + let filtered = filesFromOldest + .filter { + let fileAge = dateProvider.now.timeIntervalSince($0.fileCreationDate) + return excludedFileNames.contains($0.file.name) == false && fileAge >= performance.minFileAgeForRead + } + return filtered + .prefix(limit) + .map { $0.file } + } catch { + telemetry.error("Failed to obtain readable file", error: error) + return [] + } + } + + func delete(readableFile: ReadableFile, deletionReason: BatchDeletedMetric.RemovalReason) { + do { + try readableFile.delete() + sendBatchDeletedMetric(batchFile: readableFile, deletionReason: deletionReason) + } catch { + telemetry.error("Failed to delete file", error: error) + } + } + + /// If files age should be ignored for obtaining `ReadableFile`. + internal var ignoreFilesAgeWhenReading = false + + // MARK: - Directory size management + + /// Removes oldest files from the directory if it becomes too big. + private func purgeFilesDirectoryIfNeeded() throws { + let filesSortedByCreationDate = try directory.files() + .map { (file: $0, fileCreationDate: fileCreationDateFrom(fileName: $0.name)) } + .sorted { $0.fileCreationDate < $1.fileCreationDate } + + var filesWithSizeSortedByCreationDate = try filesSortedByCreationDate + .map { (file: $0.file, size: try $0.file.size()) } + + let accumulatedFilesSize = filesWithSizeSortedByCreationDate.map { $0.size }.reduce(0, +) + + if accumulatedFilesSize > performance.maxDirectorySize { + let sizeToFree = accumulatedFilesSize - performance.maxDirectorySize + var sizeFreed: UInt64 = 0 + + while sizeFreed < sizeToFree && !filesWithSizeSortedByCreationDate.isEmpty { + let fileWithSize = filesWithSizeSortedByCreationDate.removeFirst() + try fileWithSize.file.delete() + sendBatchDeletedMetric(batchFile: fileWithSize.file, deletionReason: .purged) + sizeFreed += fileWithSize.size + } + } + } + + private func deleteFileIfItsObsolete(file: File, fileCreationDate: Date) throws -> (file: File, fileCreationDate: Date)? { + let fileAge = dateProvider.now.timeIntervalSince(fileCreationDate) + + if fileAge > performance.maxFileAgeForRead { + try file.delete() + sendBatchDeletedMetric(batchFile: file, deletionReason: .obsolete) + return nil + } else { + return (file: file, fileCreationDate: fileCreationDate) + } + } + + // MARK: - Metrics + + /// Sends "Batch Deleted" telemetry log. + /// - Parameters: + /// - batchFile: The batch file that was deleted. + /// - deletionReason: The reason of deleting this file. + /// + /// Note: The `batchFile` doesn't exist at this point. + private func sendBatchDeletedMetric(batchFile: ReadableFile, deletionReason: BatchDeletedMetric.RemovalReason) { + guard let metricsData = metricsData, deletionReason.includeInMetric else { + return // do not track metrics for this orchestrator or deletion reason + } + + let batchAge = dateProvider.now.timeIntervalSince(fileCreationDateFrom(fileName: batchFile.name)) + + telemetry.metric( + name: BatchDeletedMetric.name, + attributes: [ + SDKMetricFields.typeKey: BatchDeletedMetric.typeValue, + BatchMetric.trackKey: metricsData.trackName, + BatchDeletedMetric.uploaderDelayKey: [ + BatchDeletedMetric.uploaderDelayMinKey: metricsData.uploaderPerformance.minUploadDelay.toMilliseconds, + BatchDeletedMetric.uploaderDelayMaxKey: metricsData.uploaderPerformance.maxUploadDelay.toMilliseconds, + ], + BatchMetric.consentKey: metricsData.consentLabel, + BatchDeletedMetric.uploaderWindowKey: performance.uploaderWindow.toMilliseconds, + BatchDeletedMetric.batchAgeKey: batchAge.toMilliseconds, + BatchDeletedMetric.batchRemovalReasonKey: deletionReason.toString(), + BatchDeletedMetric.inBackgroundKey: false, + BatchDeletedMetric.backgroundTasksEnabled: metricsData.backgroundTasksEnabled + ], + sampleRate: BatchDeletedMetric.sampleRate + ) + } + + /// Sends "Batch Closed" telemetry log. + /// - Parameters: + /// - fileName: The name of the batch that was closed. + private func sendBatchClosedMetric(fileName: String) { + guard let metricsData = metricsData else { + return // do not track metrics for this orchestrator + } + guard let lastWriteDate = lastWritableFileLastWriteDate else { + return // not reachable + } + let batchDuration = lastWriteDate.timeIntervalSince(fileCreationDateFrom(fileName: fileName)) + + telemetry.metric( + name: BatchClosedMetric.name, + attributes: [ + SDKMetricFields.typeKey: BatchClosedMetric.typeValue, + BatchMetric.trackKey: metricsData.trackName, + BatchMetric.consentKey: metricsData.consentLabel, + BatchClosedMetric.uploaderWindowKey: performance.uploaderWindow.toMilliseconds, + BatchClosedMetric.batchSizeKey: lastWritableFileApproximatedSize, + BatchClosedMetric.batchEventsCountKey: lastWritableFileObjectsCount, + BatchClosedMetric.batchDurationKey: batchDuration.toMilliseconds + ], + sampleRate: BatchClosedMetric.sampleRate + ) + } +} + +/// 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 / FilesOrchestrator.Constants.fileNamePrecision + let converted = (try? UInt64(withReportingOverflow: milliseconds)) ?? 0 + return String(converted) +} + +/// 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) * FilesOrchestrator.Constants.fileNamePrecision + return Date(timeIntervalSinceReferenceDate: TimeInterval(millisecondsSinceReferenceDate)) +} diff --git a/DatadogCore/Sources/Core/Storage/Reading/DataReader.swift b/DatadogCore/Sources/Core/Storage/Reading/DataReader.swift new file mode 100644 index 0000000000..454bcc7ada --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Reading/DataReader.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 + +/// Synchronizes the work of `FileReader` on given read/write queue. +internal final class DataReader: Reader { + /// Queue used to synchronize reads and writes for the feature. + internal let queue: DispatchQueue + private let fileReader: Reader + + init(readWriteQueue: DispatchQueue, fileReader: Reader) { + self.queue = readWriteQueue + self.fileReader = fileReader + } + + func readFiles(limit: Int) -> [ReadableFile] { + queue.sync { + self.fileReader.readFiles(limit: limit) + } + } + + func readBatch(from file: ReadableFile) -> Batch? { + queue.sync { + self.fileReader.readBatch(from: file) + } + } + + func markBatchAsRead(_ batch: Batch, reason: BatchDeletedMetric.RemovalReason) { + queue.sync { + self.fileReader.markBatchAsRead(batch, reason: reason) + } + } +} diff --git a/DatadogCore/Sources/Core/Storage/Reading/FileReader.swift b/DatadogCore/Sources/Core/Storage/Reading/FileReader.swift new file mode 100644 index 0000000000..ed4a3c5348 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Reading/FileReader.swift @@ -0,0 +1,102 @@ +/* + * 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 + +/// Reads data from files. +internal final class FileReader: Reader { + /// Orchestrator producing reference to readable file. + private let orchestrator: FilesOrchestratorType + private let encryption: DataEncryption? + /// Telemetry interface. + private let telemetry: Telemetry + + /// Files marked as read. + private var filesRead: Set = [] + + init( + orchestrator: FilesOrchestratorType, + encryption: DataEncryption?, + telemetry: Telemetry + ) { + self.orchestrator = orchestrator + self.telemetry = telemetry + self.encryption = encryption + } + + // MARK: - Reading batches + + func readFiles(limit: Int) -> [ReadableFile] { + return orchestrator.getReadableFiles(excludingFilesNamed: filesRead, limit: limit) + } + + func readBatch(from file: ReadableFile) -> Batch? { + do { + let dataBlocks = try decode(stream: file.stream()) + return Batch(dataBlocks: dataBlocks, file: file) + } catch { + telemetry.error("Failed to read data from file", error: error) + return nil + } + } + + /// Decodes input data + /// + /// The input data is expected to be a stream of `DataBlock`. Only block of type `event` are + /// consumed and decrypted if encryption is available. Decrypted events are finally joined with + /// data-format separator. + /// + /// - Parameter stream: The InputStream that provides data to decode. + /// - Returns: The decoded and formatted data. + private func decode(stream: InputStream) throws -> [BatchDataBlock] { + let reader = BatchDataBlockReader( + input: stream, + maxBlockLength: orchestrator.performance.maxObjectSize + ) + + var failure: String? = nil + defer { + failure.map { DD.logger.error($0) } + } + + return try reader.all() + .compactMap { dataBlock in + do { + return try decrypt(dataBlock: dataBlock) + } catch { + failure = "🔥 Failed to decrypt data with error: \(error)" + return nil + } + } + } + + private func decrypt(dataBlock: BatchDataBlock) throws -> BatchDataBlock { + let decrypted = try decrypt(data: dataBlock.data) + return BatchDataBlock(type: dataBlock.type, data: decrypted) + } + + /// Decrypts data if encryption is available. + /// + /// If no encryption, the data is returned. + /// + /// - Parameter data: The data to decrypt. + /// - Returns: Decrypted data. + private func decrypt(data: Data) throws -> Data { + guard let encryption = encryption else { + return data + } + + return try encryption.decrypt(data: data) + } + + // MARK: - Accepting batches + + func markBatchAsRead(_ batch: Batch, reason: BatchDeletedMetric.RemovalReason) { + orchestrator.delete(readableFile: batch.file, deletionReason: reason) + filesRead.insert(batch.file.name) + } +} diff --git a/DatadogCore/Sources/Core/Storage/Reading/Reader.swift b/DatadogCore/Sources/Core/Storage/Reading/Reader.swift new file mode 100644 index 0000000000..389233aaf4 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Reading/Reader.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 +import DatadogInternal + +internal struct Batch { + /// Data blocks in the batch. + let dataBlocks: [BatchDataBlock] + /// File from which `data` was read. + let file: ReadableFile +} + +extension Batch { + /// Events contained in the batch. + var events: [Event] { + let generator = EventGenerator(dataBlocks: dataBlocks) + return generator.map { $0 } + } +} + +/// A type, reading batched data. +internal protocol Reader { + /// Reads files from the storage. + /// - Parameter limit: maximum number of files to read. + func readFiles(limit: Int) -> [ReadableFile] + /// Reads batch from given file. + /// - Parameter file: file to read batch from. + func readBatch(from file: ReadableFile) -> Batch? + /// Marks given batch as read. + /// - Parameter batch: batch to mark as read. + /// - Parameter reason: reason for removing the batch. + func markBatchAsRead(_ batch: Batch, reason: BatchDeletedMetric.RemovalReason) +} diff --git a/DatadogCore/Sources/Core/Storage/Storage+TLV.swift b/DatadogCore/Sources/Core/Storage/Storage+TLV.swift new file mode 100644 index 0000000000..7320598228 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Storage+TLV.swift @@ -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 Foundation + +/// Default max data length in TLV block for batch file (safety check) - 10 MB +internal let MAX_DATA_LENGTH: UInt64 = 10.MB.asUInt64() + +/// TLV block type used in batch files. +internal enum BatchBlockType: UInt16 { + /// Represents an event + case event = 0x00 + /// Represents an event metadata associated with the previous event. + /// This block is optional and may be omitted. + case eventMetadata = 0x01 +} + +/// TLV data block stored in batch files. +internal typealias BatchDataBlock = TLVBlock + +/// TLV reader for batch files. +internal typealias BatchDataBlockReader = TLVBlockReader diff --git a/DatadogCore/Sources/Core/Storage/Writing/AsyncWriter.swift b/DatadogCore/Sources/Core/Storage/Writing/AsyncWriter.swift new file mode 100644 index 0000000000..8357d490f9 --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Writing/AsyncWriter.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 Foundation +import DatadogInternal + +/// Writer performing writes asynchronously on a given queue. +internal struct AsyncWriter: Writer { + private let writer: Writer + private let queue: DispatchQueue + + init(execute otherWriter: Writer, on queue: DispatchQueue) { + self.writer = otherWriter + self.queue = queue + } + + func write(value: T, metadata: M?) { + queue.async { writer.write(value: value, metadata: metadata) } + } +} + +internal struct NOPWriter: Writer { + func write(value: T, metadata: M?) { + } +} diff --git a/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift b/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift new file mode 100644 index 0000000000..eb487709df --- /dev/null +++ b/DatadogCore/Sources/Core/Storage/Writing/FileWriter.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// JSON encoder used to encode data. +private let jsonEncoder: JSONEncoder = .dd.default() + +/// Writes data to files. +internal struct FileWriter: Writer { + /// Orchestrator producing reference to writable file. + let orchestrator: FilesOrchestratorType + /// Algorithm to encrypt written data. + let encryption: DataEncryption? + /// Telemetry interface. + let telemetry: Telemetry + + init( + orchestrator: FilesOrchestratorType, + encryption: DataEncryption?, + telemetry: Telemetry + ) { + self.orchestrator = orchestrator + self.encryption = encryption + self.telemetry = telemetry + } + + // MARK: - Writing data + + /// Encodes given encodable value and metadata, and writes it to the file. + /// If encryption is available, the data is encrypted before writing. + /// - Parameters: + /// - value: Encodable value to write. + /// - metadata: Encodable metadata to write. + func write(value: T, metadata: M?) { + 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: 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("(\(orchestrator.trackName)) Failed to write \(writeSize) bytes to file", error: error) + telemetry.error("(\(orchestrator.trackName)) Failed to write \(writeSize) bytes to file", error: error) + } + } + + /// Encodes the given encodable value and encrypt it if encryption is available. + /// + /// The returned data format: + /// + /// +- 2 bytes -+- 4 bytes -+- n bytes -| + /// | 0x00 | block size | block data | + /// +-----------+------------+------------+ + /// + /// Where the 2 first bytes represents the `block type` of + /// an event. + /// + /// - Parameter event: The value to encode. + /// - Returns: Data representation of the value. + private func encode(value: T, blockType: BatchBlockType) throws -> Data { + let data = try jsonEncoder.encode(value) + return try BatchDataBlock( + type: blockType, + data: encrypt(data: data) + ).serialize( + maxLength: orchestrator.performance.maxObjectSize + ) + } + + /// Encrypts data if encryption is available. + /// + /// If no encryption, the data is returned. + /// + /// - Parameter data: The data to encrypt. + /// - Returns: Encrypted data. + private func encrypt(data: Data) throws -> Data { + guard let encryption = encryption else { + return data + } + + return try encryption.encrypt(data: data) + } +} diff --git a/DatadogCore/Sources/Core/TLV/TLVBlock.swift b/DatadogCore/Sources/Core/TLV/TLVBlock.swift new file mode 100644 index 0000000000..563c09f483 --- /dev/null +++ b/DatadogCore/Sources/Core/TLV/TLVBlock.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 Foundation +import DatadogInternal + +/// Block size binary type +internal typealias TLVBlockSize = UInt32 + +/// Reported errors while manipulating data blocks. +internal enum TLVBlockError: Error { + case readOperationFailed(streamStatus: Stream.Status, streamError: Error?) + case invalidDataType(got: Any) + case invalidByteSequence(expected: Int, got: Int) + case bytesLengthExceedsLimit(limit: UInt64) + case dataAllocationFailure + case endOfStream +} +/// A data block in defined by its type and a byte sequence. +/// +/// A block can be serialized in data stream by following TLV format. +internal struct TLVBlock where BlockType: RawRepresentable, BlockType.RawValue == UInt16 { + /// Type describing the data block. + let type: BlockType + + /// The data. + var data: Data + + /// Returns a Data block in Type-Length-Value format. + /// + /// A block follow TLV with bytes aligned such as: + /// + /// +- 2 bytes -+- 4 bytes -+- n bytes -| + /// | block type | data size (n) | data | + /// +------------+---------------+-----------+ + /// - Parameter maxLength: Maximum data length of a block. + /// - Returns: a data block in TLV. + func serialize(maxLength: UInt64 = MAX_DATA_LENGTH) throws -> Data { + var buffer = Data() + // T + withUnsafeBytes(of: type.rawValue) { buffer.append(contentsOf: $0) } + // L + guard let length = TLVBlockSize(exactly: data.count), length <= maxLength else { + throw TLVBlockError.bytesLengthExceedsLimit(limit: maxLength) + } + withUnsafeBytes(of: length) { buffer.append(contentsOf: $0) } + // V + buffer += data + return buffer + } +} diff --git a/DatadogCore/Sources/Core/TLV/TLVBlockReader.swift b/DatadogCore/Sources/Core/TLV/TLVBlockReader.swift new file mode 100644 index 0000000000..be6a89bc3c --- /dev/null +++ b/DatadogCore/Sources/Core/TLV/TLVBlockReader.swift @@ -0,0 +1,169 @@ +/* + * 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 block reader can read TLV formatted blocks from a data input. +/// +/// This class provides methods to iteratively retrieve a sequence of +/// `DataBlock`. +internal final class TLVBlockReader where BlockType: RawRepresentable, BlockType.RawValue == UInt16 { + /// The input data stream. + private let stream: InputStream + + /// Maximum data length of a block. + private let maxBlockLength: UInt64 + + /// Reads block from an input stream. + /// + /// At initilization, the reader will open the stream, it will be closed + /// when the reader instance is deallocated. + /// + /// - Parameter stream: The input stream + init( + input stream: InputStream, + maxBlockLength: UInt64 = MAX_DATA_LENGTH + ) { + self.maxBlockLength = maxBlockLength + self.stream = stream + stream.open() + } + + deinit { + stream.close() + } + + /// Reads the next data block started at current index in data input. + /// + /// This method returns `nil` when the entire data was traversed but no more + /// block could be found. + /// + /// - Throws: `TLVBlockError` while reading the input stream. + /// - Returns: The next block or nil if none could be found. + func next() throws -> TLVBlock? { + // look for the next known block + while true { + do { + return try readBlock() + } catch TLVBlockError.invalidDataType { + continue + } catch TLVBlockError.endOfStream { + // Some streams won't return false for hasBytesAvailable until a read is attempted + return nil + } catch { + throw error + } + } + } + + /// Reads all data blocks from current index in the stream. + /// + /// - Throws: `TLVBlockError` while reading the input stream. + /// - Returns: The block sequence found in the input + func all() throws -> [TLVBlock] { + var blocks: [TLVBlock] = [] + + while let block = try next() { + blocks.append(block) + } + + return blocks + } + + /// Reads `length` bytes from stream. + /// + /// - Parameter length: The number of byte to read + /// - Throws: `TLVBlockError` while reading the input stream. + /// - Returns: Data bytes from stream. + private func read(length: Int) throws -> Data { + guard length > 0 else { + return Data() + } + + // Load from stream directly to data without unnecessary copies + var data = Data(count: length) + let count: Int = try data.withUnsafeMutableBytes { + guard let buffer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + throw TLVBlockError.dataAllocationFailure + } + return stream.read(buffer, maxLength: length) + } + + if count < 0 { + throw TLVBlockError.readOperationFailed( + streamStatus: stream.streamStatus, + streamError: stream.streamError + ) + } + + if count == 0 { + throw TLVBlockError.endOfStream + } + + guard count == length else { + throw TLVBlockError.invalidByteSequence(expected: length, got: count) + } + + return data + } + + /// Reads a block. + private func readBlock() throws -> TLVBlock { + // read an entire block before inferring the data type + // to leave the stream in a usuable state if an unkown + // type was encountered. + let type = try readType() + let data = try readData() + + guard let type = BlockType(rawValue: type) else { + throw TLVBlockError.invalidDataType(got: type) + } + + return TLVBlock(type: type, data: data) + } + + /// Reads a block type. + private func readType() throws -> BlockType.RawValue { + let data = try read(length: MemoryLayout.size) + return data.withUnsafeBytes { $0.load(as: BlockType.RawValue.self) } + } + + /// Reads block data. + private func readData() throws -> Data { + let data = try read(length: MemoryLayout.size) + let size = data.withUnsafeBytes { $0.load(as: TLVBlockSize.self) } + + // even if `Int` is able to represent all `TLVBlockSize` on 64 bit + // arch, we make sure to avoid overflow and get the exact data + // length. + // Additionally check that length hasn't been corrupted and + // we don't try to generate a huge buffer. + guard let length = Int(exactly: size), length <= maxBlockLength else { + throw TLVBlockError.bytesLengthExceedsLimit(limit: maxBlockLength) + } + + return try read(length: length) + } +} +extension TLVBlockError: CustomStringConvertible { + var description: String { + switch self { + case .readOperationFailed(let status, let error): + let error = error.map { "\($0)" } ?? "(null)" + return "DataBlock read operation failed with stream status: \(status.rawValue), error: \(error)" + case .invalidDataType(let type): + return "Invalid DataBlock type: \(type)" + case .invalidByteSequence(let expected, let got): + return "Invalid bytes sequence in DataBlock: expected \(expected) bytes but got \(got)" + case .bytesLengthExceedsLimit(let limit): + return "DataBlock length exceeds limit of \(limit) bytes" + case .dataAllocationFailure: + return "Allocation failure while reading stream" + case .endOfStream: + return "Reach end of stream while reading data blocks" + } + } +} diff --git a/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift new file mode 100644 index 0000000000..5ddba5f8b7 --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/BackgroundTaskCoordinator.swift @@ -0,0 +1,108 @@ +/* + * 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 + +/// The `BackgroundTaskCoordinator` protocol provides an abstraction for managing background tasks and includes methods for registering and ending background tasks. +internal protocol BackgroundTaskCoordinator { + /// Begins a background task, requesting additional background execution time for the app. + /// Calling it multiple times will end the previous background task and start a new one. + /// It internally implements system handler for background task expiration which will end current background task. + func beginBackgroundTask() + /// Marks the end of a background task. + func endBackgroundTask() +} + +#if canImport(UIKit) +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 + func endBgTask(_ identifier: UIBackgroundTaskIdentifier) +} + +extension UIApplication: UIKitAppBackgroundTaskCoordinator { + func beginBgTask(_ handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { + return beginBackgroundTask { + handler?() + } + } + func endBgTask(_ identifier: UIBackgroundTaskIdentifier) { + endBackgroundTask(identifier) + } +} + +internal class AppBackgroundTaskCoordinator: BackgroundTaskCoordinator { + private let app: UIKitAppBackgroundTaskCoordinator? + + @ReadWriteLock + private var currentTaskId: UIBackgroundTaskIdentifier? + + internal init( + app: UIKitAppBackgroundTaskCoordinator? = UIApplication.dd.managedShared + ) { + self.app = app + } + + internal func beginBackgroundTask() { + endBackgroundTask() + currentTaskId = app?.beginBgTask { [weak self] in + guard let self = self else { + return + } + self.endBackgroundTask() + } + } + + internal func endBackgroundTask() { + guard let currentTaskId = currentTaskId else { + return + } + if currentTaskId != .invalid { + app?.endBgTask(currentTaskId) + } + self.currentTaskId = nil + } +} +#endif + +/// Bridge protocol that matches `ProcessInfo` interface for background activity. Allows easier testablity. +internal protocol ProcessInfoActivityCoordinator { + func beginActivity(options: ProcessInfo.ActivityOptions, reason: String) -> any NSObjectProtocol + func endActivity(_ activity: any NSObjectProtocol) +} + +extension ProcessInfo: ProcessInfoActivityCoordinator {} + +internal class ExtensionBackgroundTaskCoordinator: BackgroundTaskCoordinator { + private let processInfo: ProcessInfoActivityCoordinator + + @ReadWriteLock + private var currentActivity: NSObjectProtocol? + + internal init( + processInfo: ProcessInfoActivityCoordinator = ProcessInfo() + ) { + self.processInfo = processInfo + } + + internal func beginBackgroundTask() { + endBackgroundTask() + currentActivity = processInfo.beginActivity(options: [.background], reason: "Datadog SDK background upload") + } + + internal func endBackgroundTask() { + guard let currentActivity = currentActivity else { + return + } + processInfo.endActivity(currentActivity) + self.currentActivity = nil + } +} +#endif diff --git a/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift new file mode 100644 index 0000000000..9e76b72ba3 --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/DataUploadConditions.swift @@ -0,0 +1,68 @@ +/* + * 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 + +/// Tells if data upload can be performed based on given system conditions. +internal struct DataUploadConditions { + enum Blocker { + case battery(level: Int, state: BatteryStatus.State) + case lowPowerModeOn + case networkReachability(description: String) + } + + struct Constants { + /// Battery level above which data upload can be performed. + static let minBatteryLevel: Float = 0.1 + } + + /// Battery level above which data upload can be performed. + let minBatteryLevel: Float + + init(minBatteryLevel: Float = Constants.minBatteryLevel) { + self.minBatteryLevel = minBatteryLevel + } + + 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 + 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 + // plugged to Mac through lightning cable. As `.unkown` may lead to other unreliable values, + // it seems safer to arbitrary allow uploads in such case. + return blockers + } + + let batteryFullOrCharging = battery.state == .full || battery.state == .charging + let batteryLevelIsEnough = battery.level > minBatteryLevel + + if !(batteryFullOrCharging || batteryLevelIsEnough) { + blockers.append( + .battery( + level: Int(battery.level * 100), + state: battery.state + ) + ) + } + + if context.isLowPowerModeEnabled { + blockers.append(.lowPowerModeOn) + } + + return blockers + } +} diff --git a/DatadogCore/Sources/Core/Upload/DataUploadDelay.swift b/DatadogCore/Sources/Core/Upload/DataUploadDelay.swift new file mode 100644 index 0000000000..27b5f13111 --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/DataUploadDelay.swift @@ -0,0 +1,32 @@ +/* + * 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 + +/// Mutable interval used for periodic data uploads. +internal class DataUploadDelay { + private let minDelay: TimeInterval + private let maxDelay: TimeInterval + private let changeRate: Double + + private(set) var current: TimeInterval + + init(performance: UploadPerformancePreset) { + self.minDelay = performance.minUploadDelay + self.maxDelay = performance.maxUploadDelay + self.changeRate = performance.uploadDelayChangeRate + self.current = performance.initialUploadDelay + } + + func decrease() { + current = max(minDelay, current * (1.0 - changeRate)) + } + + func increase() { + current = min(current * (1.0 + changeRate), maxDelay) + } +} diff --git a/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift new file mode 100644 index 0000000000..e1a00db198 --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/DataUploadStatus.swift @@ -0,0 +1,161 @@ +/* + * 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 + +private enum HTTPResponseStatusCode: Int { + /// The request has been accepted for processing. + case accepted = 202 + /// The server cannot or will not process the request (client error). + case badRequest = 400 + /// The request lacks valid authentication credentials. + case unauthorized = 401 + /// The server understood the request but refuses to authorize it. + case forbidden = 403 + /// The server would like to shut down the connection. + case requestTimeout = 408 + /// The request entity is larger than limits defined by server. + case payloadTooLarge = 413 + /// The client has sent too many requests in a given amount of time. + case tooManyRequests = 429 + /// The server encountered an unexpected condition. + case internalServerError = 500 + /// The server received an invalid response from another server. + case badGateway = 502 + /// The server is not ready to handle the request probably because it is overloaded. + case serviceUnavailable = 503 + /// The server (a gateway or proxy) did not receive a timely response from an upstream server. + case gatewayTimeout = 504 + /// The server is unable to complete a request due to a lack of available storage space. + case insufficientStorage = 507 + /// An unexpected status code. + case unexpected = -999 + + /// If it makes sense to retry the upload finished with this status code, e.g. if data upload failed due to `503` HTTP error, we should retry it later. + var needsRetry: Bool { + switch self { + case .accepted, .badRequest, .unauthorized, .forbidden, .payloadTooLarge: + // No retry - it's either success or a client error which won't be fixed in next upload. + return false + case .requestTimeout, .tooManyRequests, .internalServerError, .serviceUnavailable: + // Retry - it's a temporary server or connection issue that might disappear on next attempt. + return true + case .badGateway, .gatewayTimeout, .insufficientStorage: + // RUM-2745: SDK is expected to not lose data upon receiving these status codes + return true + case .unexpected: + // This shouldn't happen, but if receiving an unexpected status code we do not retry. + // This is safer than retrying as we don't know if the issue is coming from the client or server. + return false + } + } +} + +/// The status of a single upload attempt. +internal struct DataUploadStatus { + /// If upload needs to be retried (`true`) because its associated data was not delivered but it may succeed + /// in the next attempt (i.e. it failed due to device leaving signal range or a temporary server unavailability occurred). + /// If set to `false` then data associated with the upload should be deleted as it does not need any more upload + /// attempts (i.e. the upload succeeded or failed due to unrecoverable client error). + let needsRetry: Bool + + // MARK: - Debug Info + + /// The actual HTTP status code received in response (`nil` if transport error). + let responseCode: Int? + + /// Upload status description printed to the console if SDK `.debug` verbosity is enabled. + let userDebugDescription: String + + let error: DataUploadError? + + let attempt: UInt +} + +extension DataUploadStatus { + // MARK: - Initialization + + 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), + attempt: attempt + ) + } + + 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), + attempt: attempt + ) + } +} + +// MARK: - Data Upload Errors + +internal enum DataUploadError: Error, Equatable { + case unauthorized + case httpError(statusCode: Int) + case networkError(error: NSError) +} + +/// A list of known NSURLError codes which should not produce error in Telemetry. +/// Receiving these codes doesn't mean SDK issue, but the network transportation scenario where the connection interrupted due to external factors. +/// These list should evolve and we may want to add more codes in there. +/// +/// Ref.: https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes +private let ignoredNSURLErrorCodes = Set([ + NSURLErrorNetworkConnectionLost, // -1005 + NSURLErrorTimedOut, // -1001 + NSURLErrorCannotParseResponse, // - 1017 + NSURLErrorNotConnectedToInternet, // -1009 + NSURLErrorCannotFindHost, // -1003 + NSURLErrorSecureConnectionFailed, // -1200 + NSURLErrorDataNotAllowed, // -1020 + NSURLErrorCannotConnectToHost, // -1004 +]) + +extension DataUploadError { + init?(status code: Int) { + guard let responseStatusCode = HTTPResponseStatusCode(rawValue: code) else { + // If status code is unexpected, do not produce an error for internal Telemetry - otherwise monitoring may + // become too verbose for old installations if we introduce a new status code in the API. + return nil + } + + switch responseStatusCode { + case .accepted: + return nil + case .unauthorized, .forbidden: + self = .unauthorized + case .internalServerError, .serviceUnavailable, .badGateway, .gatewayTimeout, .insufficientStorage: + // These codes indicate Datadog service issue - so do not produce error as there is no fix reqiured for SDK + return nil + case .badRequest, .payloadTooLarge, .tooManyRequests, .requestTimeout: + // These codes might indicate SDK issue - so produce an error so we send it through telemetry. + self = .httpError(statusCode: code) + case .unexpected: + return nil + } + } + + init?(networkError: Error) { + let nsError = networkError as NSError + guard nsError.domain == NSURLErrorDomain, !ignoredNSURLErrorCodes.contains(nsError.code) else { + return nil + } + + self = .networkError(error: nsError) + } +} diff --git a/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift new file mode 100644 index 0000000000..6e8f722ebd --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/DataUploadWorker.swift @@ -0,0 +1,245 @@ +/* + * 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 + +/// Abstracts the `DataUploadWorker`, so we can have no-op uploader in tests. +internal protocol DataUploadWorkerType { + func flushSynchronously() + func cancelSynchronously() +} + +internal class DataUploadWorker: DataUploadWorkerType { + /// Queue to execute uploads. + private let queue: DispatchQueue + /// File reader providing data to upload. + private let fileReader: Reader + /// Data uploader sending data to server. + private let dataUploader: DataUploaderType + /// Variable system conditions determining if upload should be performed. + private let uploadConditions: DataUploadConditions + /// Name of the feature this worker is performing uploads for. + private let featureName: String + /// The core context provider + private let contextProvider: DatadogContextProvider + /// Delay used to schedule consecutive uploads. + private let delay: DataUploadDelay + /// Maximum number of batches to upload in one request. + private let maxBatchesPerUpload: Int + + /// Batch reading work scheduled by this worker. + @ReadWriteLock + private var readWork: DispatchWorkItem? + /// Batch upload work scheduled by this worker. + @ReadWriteLock + private var uploadWork: DispatchWorkItem? + + /// Telemetry interface. + private let telemetry: Telemetry + + /// 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, + dataUploader: DataUploaderType, + contextProvider: DatadogContextProvider, + uploadConditions: DataUploadConditions, + delay: DataUploadDelay, + featureName: String, + telemetry: Telemetry, + maxBatchesPerUpload: Int, + backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil + ) { + self.queue = queue + self.fileReader = fileReader + self.uploadConditions = uploadConditions + self.dataUploader = dataUploader + self.contextProvider = contextProvider + self.backgroundTaskCoordinator = backgroundTaskCoordinator + self.delay = delay + self.maxBatchesPerUpload = maxBatchesPerUpload + self.featureName = featureName + self.telemetry = telemetry + let readWorkItem = DispatchWorkItem { [weak self] in + guard let self = self else { + return + } + let context = contextProvider.read() + let blockersForUpload = uploadConditions.blockersForUpload(with: context) + let isSystemReady = blockersForUpload.isEmpty + let files = isSystemReady ? fileReader.readFiles(limit: maxBatchesPerUpload) : nil + if let files = files, !files.isEmpty { + DD.logger.debug("⏳ (\(self.featureName)) Uploading batches...") + self.backgroundTaskCoordinator?.beginBackgroundTask() + self.uploadFile(from: files.reversed(), context: context) + } else { + let batchLabel = files?.isEmpty == false ? "YES" : (isSystemReady ? "NO" : "NOT CHECKED") + DD.logger.debug("💡 (\(self.featureName)) No upload. Batch to upload: \(batchLabel), System conditions: \(blockersForUpload.description)") + self.delay.increase() + self.backgroundTaskCoordinator?.endBackgroundTask() + self.scheduleNextCycle() + } + } + self.readWork = readWorkItem + + // Start sending batches immediately after initialization: + queue.async(execute: readWorkItem) + } + + private func scheduleNextCycle() { + guard let readWork = self.readWork else { + return + } + queue.asyncAfter(deadline: .now() + delay.current, execute: readWork) + } + + private func uploadFile(from files: [ReadableFile], context: DatadogContext) { + let uploadWork = DispatchWorkItem { [weak self] in + guard let self = self else { + return + } + var files = files + guard let file = files.popLast() else { + self.scheduleNextCycle() + return + } + if let batch = self.fileReader.readBatch(from: file) { + do { + let uploadStatus = try self.dataUploader.upload( + events: batch.events, + context: context, + previous: previousUploadStatus + ) + previousUploadStatus = uploadStatus + + if uploadStatus.needsRetry { + DD.logger.debug(" → (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus.userDebugDescription)") + self.delay.increase() + self.scheduleNextCycle() + return + } else { + DD.logger.debug(" → (\(self.featureName)) accepted, won't be retransmitted: \(uploadStatus.userDebugDescription)") + if files.isEmpty { + self.delay.decrease() + } + self.fileReader.markBatchAsRead( + batch, + reason: .intakeCode(responseCode: uploadStatus.responseCode) + ) + previousUploadStatus = nil + } + + if let error = uploadStatus.error { + switch error { + case .unauthorized: + DD.logger.error("⚠️ Make sure that the provided token still exists and you're targeting the relevant Datadog site.") + case let .httpError(statusCode: statusCode): + self.telemetry.error("Data upload finished with status code: \(statusCode)") + case let .networkError(error: error): + self.telemetry.error("Data upload finished with error", error: error) + } + } + } 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) + } + } + if files.isEmpty { + self.scheduleNextCycle() + } else { + self.uploadFile(from: files, context: context) + } + } + self.uploadWork = uploadWork + queue.async(execute: uploadWork) + } + + /// Sends all unsent data synchronously. + /// - It performs arbitrary upload (without checking upload condition and without re-transmitting failed uploads). + internal func flushSynchronously() { + queue.sync { [weak self] in + guard let self = self else { + return + } + for file in self.fileReader.readFiles(limit: .max) { + guard let nextBatch = self.fileReader.readBatch(from: file) else { + continue + } + defer { + // RUMM-3459 Delete the underlying batch with `.flushed` reason that will be ignored in reported + // 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. + previousUploadStatus = try self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) + } catch { + previousUploadStatus = try? self.dataUploader.upload( + events: nextBatch.events, + context: self.contextProvider.read(), + previous: previousUploadStatus + ) + } + } + } + } + + /// Cancels scheduled uploads and stops scheduling next ones. + /// - It does not affect the upload that has already begun. + /// - It blocks the caller thread if called in the middle of upload execution. + internal func cancelSynchronously() { + queue.sync { [weak self] in + guard let self = self else { + return + } + // This cancellation must be performed on the `queue` to ensure that it is not called + // in the middle of a `DispatchWorkItem` execution - otherwise, as the pending block would be + // fully executed, it will schedule another upload by calling `nextScheduledWork(after:)` at the end. + self.uploadWork?.cancel() + self.uploadWork = nil + self.readWork?.cancel() + self.readWork = nil + } + } +} + +extension DataUploadConditions.Blocker: CustomStringConvertible { + var description: String { + switch self { + case let .battery(level: level, state: state): + return "🔋 Battery state is: \(state) (\(level)%)" + case .lowPowerModeOn: + return "🔌 Low Power Mode is: enabled" + case let .networkReachability(description: description): + return "📡 Network reachability is: " + description + } + } +} + +fileprivate extension Array where Element == DataUploadConditions.Blocker { + var description: String { + if self.isEmpty { + return "✅" + } else { + return "❌ [upload was skipped because: " + map { + $0.description + }.joined(separator: " AND ") + "]" + } + } +} diff --git a/DatadogCore/Sources/Core/Upload/DataUploader.swift b/DatadogCore/Sources/Core/Upload/DataUploader.swift new file mode 100644 index 0000000000..0c89d211fc --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/DataUploader.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 CommonCrypto + +/// A type that performs data uploads. +internal protocol DataUploaderType { + func upload(events: [Event], context: DatadogContext, previous: DataUploadStatus?) throws -> DataUploadStatus +} + +/// Synchronously uploads data to server using `HTTPClient`. +internal final class DataUploader: DataUploaderType { + /// An unreachable upload status - only meant to satisfy the compiler. + private static let unreachableUploadStatus = DataUploadStatus( + needsRetry: false, + responseCode: nil, + userDebugDescription: "", + error: nil, + attempt: 0 + ) + + private let httpClient: HTTPClient + private let requestBuilder: FeatureRequestBuilder + + init(httpClient: HTTPClient, requestBuilder: FeatureRequestBuilder) { + self.httpClient = httpClient + self.requestBuilder = requestBuilder + } + + /// Uploads data synchronously (will block current thread) and returns the upload status. + /// Uses timeout configured for `HTTPClient`. + 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? + + let semaphore = DispatchSemaphore(value: 0) + + httpClient.send(request: request) { result in + switch result { + case .success(let httpResponse): + uploadStatus = DataUploadStatus( + httpResponse: httpResponse, + ddRequestID: requestID, + attempt: attempt + ) + case .failure(let error): + uploadStatus = DataUploadStatus( + networkError: error, + attempt: attempt + ) + } + + semaphore.signal() + } + + _ = semaphore.wait(timeout: .distantFuture) + + return uploadStatus ?? DataUploader.unreachableUploadStatus + } +} diff --git a/DatadogCore/Sources/Core/Upload/FeatureUpload.swift b/DatadogCore/Sources/Core/Upload/FeatureUpload.swift new file mode 100644 index 0000000000..5d6f4304cc --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/FeatureUpload.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 Foundation +import DatadogInternal + +internal struct FeatureUpload { + /// Uploads data to server. + let uploader: DataUploadWorkerType + + init( + featureName: String, + contextProvider: DatadogContextProvider, + fileReader: Reader, + requestBuilder: FeatureRequestBuilder, + httpClient: HTTPClient, + performance: PerformancePreset, + backgroundTasksEnabled: Bool, + maxBatchesPerUpload: Int, + isRunFromExtension: Bool, + telemetry: Telemetry + ) { + let uploadQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-\(featureName)-upload", + autoreleaseFrequency: .workItem, + target: .global(qos: .utility) + ) + + let dataUploader = DataUploader( + httpClient: httpClient, + requestBuilder: requestBuilder + ) + + #if canImport(UIKit) + 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, _): + backgroundTaskCoordinator = nil + } + #else + let backgroundTaskCoordinator: BackgroundTaskCoordinator? = nil + #endif + + self.init( + uploader: DataUploadWorker( + queue: uploadQueue, + fileReader: fileReader, + dataUploader: dataUploader, + contextProvider: contextProvider, + uploadConditions: DataUploadConditions(), + delay: DataUploadDelay(performance: performance), + featureName: featureName, + telemetry: telemetry, + maxBatchesPerUpload: maxBatchesPerUpload, + backgroundTaskCoordinator: backgroundTaskCoordinator + ) + ) + } + + init(uploader: DataUploadWorkerType) { + self.uploader = uploader + } + + /// Flushes all authorised data and tears down the upload stack. + /// - It completes all pending asynchronous work in upload worker and cancels its next schedules. + /// - It flushes all data stored in authorized files by performing their arbitrary upload (without retrying). + /// + /// This method is executed synchronously. After return, the upload feature has no more + /// pending asynchronous operations and all its authorized data should be considered uploaded. + internal func flushAndTearDown() { + uploader.cancelSynchronously() + uploader.flushSynchronously() + } +} diff --git a/DatadogCore/Sources/Core/Upload/HTTPClient.swift b/DatadogCore/Sources/Core/Upload/HTTPClient.swift new file mode 100644 index 0000000000..ba8407817f --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/HTTPClient.swift @@ -0,0 +1,16 @@ +/* + * 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 type responsible for sending HTTP requests. +internal protocol HTTPClient { + /// Sends the provided request using HTTP. + /// - Parameters: + /// - request: The request to be sent. + /// - completion: A closure that receives a Result containing either an HTTPURLResponse or an Error. + func send(request: URLRequest, completion: @escaping (Result) -> Void) +} diff --git a/DatadogCore/Sources/Core/Upload/URLSessionClient.swift b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift new file mode 100644 index 0000000000..0523a44af9 --- /dev/null +++ b/DatadogCore/Sources/Core/Upload/URLSessionClient.swift @@ -0,0 +1,75 @@ +/* + * 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 + +/// Client for sending requests over HTTP. +internal class URLSessionClient: HTTPClient { + internal let session: URLSession + + convenience init(proxyConfiguration: [AnyHashable: Any]? = nil) { + let configuration: URLSessionConfiguration = .ephemeral + // NOTE: RUMM-610 Default behaviour of `.ephemeral` session is to cache requests. + // To not leak requests memory (including their `.httpBody` which may be significant) + // we explicitly opt-out from using cache. This cannot be achieved using `.requestCachePolicy`. + 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, + let password = proxyConfiguration?[kCFProxyPasswordKey] as? String { + let authorization = basicHTTPAuthentication(username: user, password: password) + configuration.httpAdditionalHeaders = ["Proxy-Authorization": authorization] + } + #endif + + self.init(session: URLSession(configuration: configuration)) + } + + init(session: URLSession) { + self.session = session + } + + func send(request: URLRequest, completion: @escaping (Result) -> Void) { + let task = session.dataTask(with: request) { data, response, error in + completion(httpClientResult(for: (data, response, error))) + } + task.resume() + } +} + +/// An error returned if `URLSession` response state is inconsistent (like no data, no response and no error). +/// The code execution in `URLSessionTransport` should never reach its initialization. +internal struct URLSessionTransportInconsistencyException: Error {} + +/// Returns a `Basic` `Authorization` header using the `username` and `password` provided. +/// +/// - Parameters: +/// - username: The username of the header. +/// - password: The password of the header. +/// - Returns: The HTTP Basic authentication header value +private func basicHTTPAuthentication(username: String, password: String) -> String { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + return "Basic \(credential)" +} + +/// As `URLSession` returns 3-values-tuple for request execution, this function applies consistency constraints and turns +/// it into only two possible states of `HTTPTransportResult`. +private func httpClientResult(for urlSessionTaskCompletion: (Data?, URLResponse?, Error?)) -> Result { + let (_, response, error) = urlSessionTaskCompletion + + if let error = error { + return .failure(error) + } + + if let httpResponse = response as? HTTPURLResponse { + return .success(httpResponse) + } + + return .failure(URLSessionTransportInconsistencyException()) +} diff --git a/DatadogCore/Sources/Datadog+Internal.swift b/DatadogCore/Sources/Datadog+Internal.swift new file mode 100644 index 0000000000..ecf30fd50e --- /dev/null +++ b/DatadogCore/Sources/Datadog+Internal.swift @@ -0,0 +1,58 @@ +/* + * 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 + +extension Datadog: InternalExtended {} + +/// This extension exposes internal methods that are used by other Datadog modules and cross platform +/// frameworks. It is not meant for public use. +/// +/// DO NOT USE this extension or its methods if you are not working on the internals of the Datadog SDK +/// or one of the cross platform frameworks. +/// +/// Methods, members, and functionality of this class are subject to change without notice, as they +/// are not considered part of the public interface of the Datadog SDK. +extension InternalExtension where ExtendedType == Datadog { + /// Internal telemetry proxy. + public static var telemetry: _TelemetryProxy { + .init(telemetry: CoreRegistry.default.telemetry) + } + + /// Changes the `version` used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + public static func set(customVersion: String) { + guard let core = CoreRegistry.default as? DatadogCore else { + return + } + + core.applicationVersionPublisher.version = customVersion + } +} + +public struct _TelemetryProxy { + let telemetry: Telemetry + + /// See Telementry.debug + public func debug(id: String, message: String) { + telemetry.debug(id: id, message: message) + } + + /// See Telementry.error + public func error(id: String, message: String, kind: String?, stack: String?) { + telemetry.error(id: id, message: message, kind: kind ?? "unknown", stack: stack ?? "unknown") + } +} + +extension Datadog.Configuration: InternalExtended { } +extension InternalExtension where ExtendedType == Datadog.Configuration { + /// Sets additional configuration attributes. + /// This can be used to tweak internal features of the SDK. + public var additionalConfiguration: [String: Any] { + get { type.additionalConfiguration } + set { type.additionalConfiguration = newValue} + } +} diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift new file mode 100644 index 0000000000..a731ff8a09 --- /dev/null +++ b/DatadogCore/Sources/Datadog.swift @@ -0,0 +1,577 @@ +/* + * 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 + +//swiftlint:disable duplicate_imports +@_exported import enum DatadogInternal.TrackingConsent +@_exported import protocol DatadogInternal.DatadogCoreProtocol +//swiftlint:enable duplicate_imports + +/// An entry point to Datadog SDK. +/// +/// Initialize the core instance of the Datadog SDK prior to enabling any Product. +/// +/// ```swift +/// Datadog.initialize( +/// with: Datadog.Configuration(clientToken: "", env: ""), +/// trackingConsent: .pending +/// ) +/// ``` +/// +/// Once Datadog SDK is initialized, you can enable products, such as RUM: +/// +/// ```swift +/// RUM.enable( +/// with: RUM.Configuration(applicationID: "") +/// ) +/// ``` +/// +public enum Datadog { + /// Configuration of Datadog SDK. + public struct Configuration { + /// Defines the Datadog SDK policy when batching data together before uploading it to Datadog servers. + /// Smaller batches mean smaller but more network requests, whereas larger batches mean fewer but larger network requests. + public enum BatchSize { + /// Prefer small sized data batches. + case small + /// Prefer medium sized data batches. + case medium + /// Prefer large sized data batches. + case large + } + + /// Defines the frequency at which Datadog SDK will try to upload data batches. + public enum UploadFrequency { + /// Try to upload batched data frequently. + case frequent + /// Try to upload batched data with a medium frequency. + case average + /// Try to upload batched data rarely. + case rare + } + + /// Defines the maximum amount of batches processed sequentially without a delay within one reading/uploading cycle. + public enum BatchProcessingLevel { + case low + case medium + case high + + var maxBatchesPerUpload: Int { + switch self { + case .low: + return 1 + case .medium: + return 10 + case .high: + return 100 + } + } + } + + /// Either the RUM client token (which supports RUM, Logging and APM) or regular client token, only for Logging and APM. + public var clientToken: String + + /// The environment name which will be sent to Datadog. This can be used + /// To filter events on different environments (e.g. "staging" or "production"). + public var env: String + + /// The Datadog server site where data is sent. + /// + /// Default value is `.us1`. + public var site: DatadogSite + + /// The service name associated with data send to Datadog. + /// + /// Default value is set to application bundle identifier. + public var service: String? + + /// The preferred size of batched data uploaded to Datadog servers. + /// This value impacts the size and number of requests performed by the SDK. + /// + /// `.medium` by default. + public var batchSize: BatchSize + + /// The preferred frequency of uploading data to Datadog servers. + /// This value impacts the frequency of performing network requests by the SDK. + /// + /// `.average` by default. + public var uploadFrequency: UploadFrequency + + /// Proxy configuration attributes. + /// This can be used to a enable a custom proxy for uploading tracked data to Datadog's intake. + /// + /// Ref.: https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411499-connectionproxydictionary + public var proxyConfiguration: [AnyHashable: Any]? + + /// SeData encryption to use for on-disk data persistency by providing an object + /// complying with `DataEncryption` protocol. + public var encryption: DataEncryption? + + /// A custom NTP synchronization interface. + /// + /// By default, the Datadog SDK synchronizes with dedicated NTP pools provided by the + /// https://www.ntppool.org/ . Using different pools or setting a no-op `ServerDateProvider` + /// implementation will result in desynchronization of the SDK instance and the Datadog servers. + /// This can lead to significant time shift in RUM sessions or distributed traces. + public var serverDateProvider: ServerDateProvider + + /// The bundle object that contains the current executable. + public var bundle: Bundle + + /// Batch provessing level, defining the maximum number of batches processed sequencially without a delay within one reading/uploading cycle. + /// + /// `.medium` by default. + public var batchProcessingLevel: BatchProcessingLevel + + /// Flag that determines if UIApplication methods [`beginBackgroundTask(expirationHandler:)`](https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) and [`endBackgroundTask:`](https://developer.apple.com/documentation/uikit/uiapplication/1622970-endbackgroundtask) + /// are utilized to perform background uploads. It may extend the amount of time the app is operating in background by 30 seconds. + /// + /// Tasks are normally stopped when there's nothing to upload or when encountering any upload blocker such us no internet connection or low battery. + /// + /// `false` by default. + public var backgroundTasksEnabled: Bool + + /// Creates a Datadog SDK Configuration object. + /// + /// - Parameters: + /// - clientToken: Either the RUM client token (which supports RUM, Logging and APM) or regular client token, + /// only for Logging and APM. + /// + /// - env: The environment name which will be sent to Datadog. This can be used + /// To filter events on different environments (e.g. "staging" or "production"). + /// + /// - site: Datadog site endpoint, default value is `.us1`. + /// + /// - service: The service name associated with data send to Datadog. + /// Default value is set to application bundle identifier. + /// + /// - bundle: The bundle object that contains the current executable. + /// + /// - batchSize: The preferred size of batched data uploaded to Datadog servers. + /// This value impacts the size and number of requests performed by the SDK. + /// `.medium` by default. + /// + /// - uploadFrequency: The preferred frequency of uploading data to Datadog servers. + /// This value impacts the frequency of performing network requests by the SDK. + /// `.average` by default. + /// + /// - proxyConfiguration: A proxy configuration attributes. + /// This can be used to a enable a custom proxy for uploading tracked data to Datadog's intake. + /// + /// - encryption: Data encryption to use for on-disk data persistency by providing an object + /// complying with `DataEncryption` protocol. + /// + /// - serverDateProvider: A custom NTP synchronization interface. + /// By default, the Datadog SDK synchronizes with dedicated NTP pools provided by the + /// https://www.ntppool.org/ . Using different pools or setting a no-op `ServerDateProvider` + /// implementation will result in desynchronization of the SDK instance and the Datadog servers. + /// This can lead to significant time shift in RUM sessions or distributed traces. + /// - backgroundTasksEnabled: A flag that determines if `UIApplication` methods + /// `beginBackgroundTask(expirationHandler:)` and `endBackgroundTask:` + /// are used to perform background uploads. + /// It may extend the amount of time the app is operating in background by 30 seconds. + /// Tasks are normally stopped when there's nothing to upload or when encountering + /// any upload blocker such us no internet connection or low battery. + /// By default it's set to `false`. + public init( + clientToken: String, + env: String, + site: DatadogSite = .us1, + service: String? = nil, + bundle: Bundle = .main, + batchSize: BatchSize = .medium, + uploadFrequency: UploadFrequency = .average, + proxyConfiguration: [AnyHashable: Any]? = nil, + encryption: DataEncryption? = nil, + serverDateProvider: ServerDateProvider? = nil, + batchProcessingLevel: BatchProcessingLevel = .medium, + backgroundTasksEnabled: Bool = false + ) { + self.clientToken = clientToken + self.env = env + self.site = site + self.service = service + self.bundle = bundle + self.batchSize = batchSize + self.uploadFrequency = uploadFrequency + self.proxyConfiguration = proxyConfiguration + self.encryption = encryption + self.serverDateProvider = serverDateProvider ?? DatadogNTPDateProvider() + self.batchProcessingLevel = batchProcessingLevel + self.backgroundTasksEnabled = backgroundTasksEnabled + } + + // MARK: - Internal + + /// Obtains OS directory where SDK creates its root folder. + /// All instances of the SDK use the same root folder, but each creates its own subdirectory. + internal var systemDirectory: () throws -> Directory = { try Directory.cache() } + + /// Default process information. + internal var processInfo: ProcessInfo = .processInfo + + /// Sets additional configuration attributes. + /// This can be used to tweak internal features of the SDK. + internal var additionalConfiguration: [String: Any] = [:] + + /// Default date provider used by the SDK and all products. + internal var dateProvider: DateProvider = SystemDateProvider() + + /// Creates `HTTPClient` with given proxy configuration attributes. + 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. + /// If set, internal events occuring inside SDK will be printed to debugger console if their level is equal or greater than `verbosityLevel`. + /// Default is `nil`. + public static var verbosityLevel: CoreLoggerLevel? { + get { _verbosityLevel.wrappedValue } + set { _verbosityLevel.wrappedValue = newValue } + } + + /// The backing storage for `verbosityLevel`, ensuring efficient synchronized + /// read/write access to the shared value. + private static let _verbosityLevel = ReadWriteLock(wrappedValue: nil) + + /// Returns `true` if the Datadog SDK is already initialized, `false` otherwise. + /// + /// - Parameter name: The name of the SDK instance to verify. + public static func isInitialized(instanceName name: String = CoreRegistry.defaultInstanceName) -> Bool { + CoreRegistry.instance(named: name) is DatadogCore + } + + /// Returns the Datadog SDK instance for the given name. + /// + /// - Parameter name: The name of the instance to get. + /// - Returns: The core instance if it exists, `NOPDatadogCore` instance otherwise. + public static func sdkInstance(named name: String) -> DatadogCoreProtocol { + CoreRegistry.instance(named: name) + } + + /// 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 + /// - email: User's email, if any + /// - extraInfo: User's custom attributes, if any + public static func setUserInfo( + id: String? = nil, + name: String? = nil, + email: String? = nil, + extraInfo: [AttributeKey: AttributeValue] = [:], + in core: DatadogCoreProtocol = CoreRegistry.default + ) { + let core = core as? DatadogCore + core?.setUserInfo( + id: id, + name: name, + email: email, + extraInfo: extraInfo + ) + } + + /// Add custom attributes to the current user information + /// + /// This extra info will be added to already existing extra info that is added + /// to logs traces and RUM events automatically. + /// + /// - Parameters: + /// - extraInfo: User's additionall custom attributes + public static func addUserExtraInfo( + _ extraInfo: [AttributeKey: AttributeValue?], + in core: DatadogCoreProtocol = CoreRegistry.default + ) { + let core = core as? DatadogCore + core?.addUserExtraInfo(extraInfo) + } + + /// 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 + public static func set(trackingConsent: TrackingConsent, in core: DatadogCoreProtocol = CoreRegistry.default) { + let core = core as? DatadogCore + core?.set(trackingConsent: trackingConsent) + } + + /// Clears all data that has not already been sent to Datadog servers. + public static func clearAllData(in core: DatadogCoreProtocol = CoreRegistry.default) { + let core = core as? DatadogCore + core?.clearAllData() + } + + /// Stops the initialized SDK instance attached to the given name. + /// + /// Stopping a core instance will stop all current processes by deallocating all Features registered + /// in the core as well as their storage & upload units. + /// + /// - Parameter instanceName: the name of the instance to stop. + public static func stopInstance(named instanceName: String = CoreRegistry.defaultInstanceName) { + let core = CoreRegistry.unregisterInstance(named: instanceName) as? DatadogCore + core?.stop() + } + + /// Initializes the Datadog SDK. + /// + /// You **must** initialize the core instance of the Datadog SDK prior to enabling any Product. + /// + /// ```swift + /// Datadog.initialize( + /// with: Datadog.Configuration(clientToken: "", env: ""), + /// trackingConsent: .pending + /// ) + /// ``` + /// + /// Once Datadog SDK is initialized, you can enable products, such as RUM: + /// + /// ```swift + /// RUM.enable( + /// with: RUM.Configuration(applicationID: "") + /// ) + /// ``` + /// It is possible to initialize multiple instances of the SDK, associating them with a name. + /// Many methods of the SDK can optionally take a SDK instance as an argument. If not provided, + /// the call will be associated with the default (nameless) SDK instance. + /// + /// To use a secondary instance of the SDK, provide a name to the ``initialize`` method + /// and use the returned instance to enable products: + /// + /// ```swift + /// let core = Datadog.initialize( + /// with: Datadog.Configuration(clientToken: "", env: ""), + /// trackingConsent: .pending, + /// instanceName: "my-instance" + /// ) + /// + /// RUM.enable( + /// with: RUM.Configuration(applicationID: ""), + /// in: core + /// ) + /// ``` + /// + /// - Parameters: + /// - configuration: the SDK configuration. + /// - trackingConsent: the initial state of the Data Tracking Consent given by the user of the app. + /// - instanceName: The core instance name. This value will be used for data persistency and should be + /// stable between application runs. + @discardableResult + public static func initialize( + with configuration: Configuration, + trackingConsent: TrackingConsent, + instanceName: String = CoreRegistry.defaultInstanceName + ) -> DatadogCoreProtocol { + #if targetEnvironment(macCatalyst) + consolePrint("⚠️ Catalyst is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) + #endif + + #if os(macOS) + consolePrint("⚠️ macOS is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) + #endif + + #if swift(>=5.9) && os(visionOS) + consolePrint("⚠️ visionOS is not officially supported by Datadog SDK: some features may NOT be functional!", .warn) + #endif + + do { + return try initializeOrThrow( + with: configuration, + trackingConsent: trackingConsent, + instanceName: instanceName + ) + } catch { + consolePrint("\(error)", .error) + return NOPDatadogCore() + } + } + + private static func initializeOrThrow( + with configuration: Configuration, + trackingConsent: TrackingConsent, + instanceName: String + ) throws -> DatadogCoreProtocol { + guard !CoreRegistry.isRegistered(instanceName: instanceName) else { + 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) + Datadog.verbosityLevel = .debug + } + + let applicationVersion = configuration.additionalConfiguration[CrossPlatformAttributes.version] as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0.0.0" + + let applicationBuildNumber = configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0" + + let bundleName = configuration.bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + let bundleType = BundleType(bundle: configuration.bundle) + let bundleIdentifier = configuration.bundle.bundleIdentifier ?? "unknown" + let service = configuration.service ?? configuration.bundle.bundleIdentifier ?? "ios" + let source = configuration.additionalConfiguration[CrossPlatformAttributes.ddsource] as? String ?? "ios" + let variant = configuration.additionalConfiguration[CrossPlatformAttributes.variant] as? String + let sdkVersion = configuration.additionalConfiguration[CrossPlatformAttributes.sdkVersion] as? String ?? __sdkVersion + let buildId = configuration.additionalConfiguration[CrossPlatformAttributes.buildId] as? String + let nativeSourceType = configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] as? String + + let performance = PerformancePreset( + batchSize: debug ? .small : configuration.batchSize, + uploadFrequency: debug ? .frequent : configuration.uploadFrequency, + bundleType: bundleType + ) + let isRunFromExtension = bundleType == .iOSAppExtension + + self.init( + directory: try CoreDirectory( + in: configuration.systemDirectory(), + instanceName: instanceName, + site: configuration.site + ), + dateProvider: configuration.dateProvider, + initialConsent: trackingConsent, + performance: performance, + httpClient: configuration.httpClientFactory(configuration.proxyConfiguration), + encryption: configuration.encryption, + contextProvider: DatadogContextProvider( + site: configuration.site, + clientToken: configuration.clientToken, + service: service, + env: configuration.env, + version: applicationVersion, + buildNumber: applicationBuildNumber, + buildId: buildId, + variant: variant, + source: source, + nativeSourceOverride: nativeSourceType, + sdkVersion: sdkVersion, + ciAppOrigin: CITestIntegration.active?.origin, + applicationName: bundleName ?? bundleType.rawValue, + applicationBundleIdentifier: bundleIdentifier, + applicationBundleType: bundleType, + applicationVersion: applicationVersion, + sdkInitDate: configuration.dateProvider.now, + device: DeviceInfo(processInfo: configuration.processInfo), + processInfo: configuration.processInfo, + dateProvider: configuration.dateProvider, + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter, + appStateProvider: configuration.appStateProvider + ), + applicationVersion: applicationVersion, + maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + isRunFromExtension: isRunFromExtension + ) + + telemetry.configuration( + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), + batchSize: performance.uploaderWindow.toInt64Milliseconds, + batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, + useLocalEncryption: configuration.encryption != nil, + useProxy: configuration.proxyConfiguration != nil + ) + } +} diff --git a/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift b/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.swift new file mode 100644 index 0000000000..ea08775908 --- /dev/null +++ b/DatadogCore/Sources/FeaturesIntegration/CITestIntegration.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 Foundation + +private enum DDCFMessageID { + static let setCustomTags: Int32 = 0x1111 + static let enableRUM: Int32 = 0x2222 + static let forceFlush: Int32 = 0x3333 +} + +internal class CITestIntegration { + /// Current and active integration with CIApp. + /// `nil` if the integration is not enabled. + static let active: CITestIntegration? = CITestIntegration() + /// RUMCITest model to be attached to events, it contains the CI context + let testExecutionId: String + /// Tag that must be added to spans and headers when running inside a CIApp test + let origin = "ciapp-test" + // UUID for test process message channel + private let messageChannelUUID: String? + + private init?(processInfo: ProcessInfo = .processInfo) { + guard let testID = processInfo.environment["CI_VISIBILITY_TEST_EXECUTION_ID"] else { + return nil + } + self.testExecutionId = testID + self.messageChannelUUID = processInfo.environment["CI_VISIBILITY_MESSAGE_CHANNEL_UUID"] + } + + /// Entry point for running all the tasks needed for CIApp integration + func startIntegration() { + startMessageListener() + notifyRUMSession() + } + + /// Notifies the CIApp framework that a RUM session is being started. It sends a message to a CFMessagePort that is + /// created in the CIApp framework + private func notifyRUMSession() { + let timeout: CFTimeInterval = 1.0 + + guard let remotePort = CFMessagePortCreateRemote( + nil, messagePortId(name: "DatadogTestingPort") + ) else { + return + } + CFMessagePortSendRequest( + remotePort, + DDCFMessageID.enableRUM, // Message ID for notifying the test that rum is enabled + nil, + timeout, + timeout, + nil, + nil + ) + } + + /// Creates a CFMessagePort that is used by the CIApp framework to notify that a test is going to finish, so all + /// information must be flushed to the backend. It uses a non public API `internalFlushAndDeinitialize()` + private func startMessageListener() { + func attributeCallback(port: CFMessagePort?, msgid: Int32, data: CFData?, info: UnsafeMutableRawPointer?) -> Unmanaged? { + switch msgid { + case DDCFMessageID.forceFlush: + Datadog.internalFlushAndDeinitialize() + default: + break + } + return nil + } + + guard let port = CFMessagePortCreateLocal( + nil, messagePortId(name: "DatadogRUMTestingPort"), attributeCallback, nil, nil + ) else { + return + } + let runLoopSource = CFMessagePortCreateRunLoopSource(nil, port, 0) + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes) + } + + /// Creates a ID for message port. If UUID is provided joins name with UUID. + /// Fallbacks to name if UUID is nil for backward compatibility + private func messagePortId(name: String) -> CFString { + (messageChannelUUID.map { "\(name)-\($0)" } ?? name) as CFString + } +} diff --git a/DatadogCore/Sources/Kronos/KronosClock.swift b/DatadogCore/Sources/Kronos/KronosClock.swift new file mode 100644 index 0000000000..f5e54d40ca --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosClock.swift @@ -0,0 +1,160 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Struct that has time + related metadata +internal typealias KronosAnnotatedTime = ( + /// Time that is being annotated + date: Date, + + /// Amount of time that has passed since the last NTP sync; in other words, the NTP response age. + timeSinceLastNtpSync: TimeInterval +) + +internal protocol KronosClockProtocol { + /// The most accurate date that we have so far (nil if no synchronization was done yet) + var now: Date? { get } + + /// Syncs the clock using NTP. Note that the full synchronization could take a few seconds. The given + /// closure will be called with the first valid NTP response which accuracy should be good enough for the + /// initial clock adjustment but it might not be the most accurate representation. After calling the + /// closure this method will continue syncing with multiple servers and multiple passes. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers that will be used for + /// the synchronization. + /// - parameter samples: The number of samples to be acquired from each server. + /// - parameter first: A closure that will be called after the first valid date is calculated. + /// - parameter completion: A closure that will be called after _all_ the NTP calls are finished. + func sync( + from pool: String, + samples: Int, + first: ((Date, TimeInterval) -> Void)?, + completion: ((Date?, TimeInterval?) -> Void)? + ) +} + +extension KronosClockProtocol { + /// Syncs the clock using NTP. Note that the full synchronization could take a few seconds. The given + /// closure will be called with the first valid NTP response which accuracy should be good enough for the + /// initial clock adjustment but it might not be the most accurate representation. After calling the + /// closure this method will continue syncing with multiple servers and multiple passes. 4 samples per + /// server will be acquired. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers that will be used for + /// the synchronization. + /// - parameter first: A closure that will be called after the first valid date is calculated. + /// - parameter completion: A closure that will be called after _all_ the NTP calls are finished. + func sync( + from pool: String, + first: ((Date, TimeInterval) -> Void)?, + completion: ((Date?, TimeInterval?) -> Void)? + ) { + sync(from: pool, samples: 4, first: first, completion: completion) + } +} + +/// High level implementation for clock synchronization using NTP. All returned dates use the most accurate +/// synchronization and it's not affected by clock changes. The NTP synchronization implementation has sub- +/// second accuracy but given that Darwin doesn't support microseconds on bootTime, dates don't have sub- +/// second accuracy. +/// +/// Example usage: +/// +/// ```swift +/// KronosClock.sync { date, offset in +/// print(date) +/// } +/// // (... later on ...) +/// print(KronosClock.now) +/// ``` +internal final class KronosClock: KronosClockProtocol { + private var stableTime: KronosTimeFreeze? { + didSet { + self.storage.stableTime = self.stableTime + } + } + + /// Determines where the most current stable time is stored. Use TimeStoragePolicy.appGroup to share + /// between your app and an extension. + private var storage = KronosTimeStorage(storagePolicy: .standard) + + /// The most accurate timestamp that we have so far (nil if no synchronization was done yet) + var timestamp: TimeInterval? { + return self.stableTime?.adjustedTimestamp + } + + /// The most accurate date that we have so far (nil if no synchronization was done yet) + var now: Date? { + return self.annotatedNow?.date + } + + /// Same as `now` except with analytic metadata about the time + var annotatedNow: KronosAnnotatedTime? { + guard let stableTime = self.stableTime else { + return nil + } + + return KronosAnnotatedTime( + date: Date(timeIntervalSince1970: stableTime.adjustedTimestamp), + timeSinceLastNtpSync: stableTime.timeSinceLastNtpSync + ) + } + + /// Syncs the clock using NTP. Note that the full synchronization could take a few seconds. The given + /// closure will be called with the first valid NTP response which accuracy should be good enough for the + /// initial clock adjustment but it might not be the most accurate representation. After calling the + /// closure this method will continue syncing with multiple servers and multiple passes. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers that will be used for + /// the synchronization. + /// - parameter samples: The number of samples to be acquired from each server (default 4). + /// - parameter first: A closure that will be called after the first valid date is calculated. + /// - parameter completion: A closure that will be called after _all_ the NTP calls are finished. + func sync( + from pool: String = "time.apple.com", + samples: Int = 4, + first: ((Date, TimeInterval) -> Void)? = nil, + completion: ((Date?, TimeInterval?) -> Void)? = nil + ) { + self.loadFromDefaults() + + KronosNTPClient().query(pool: pool, numberOfSamples: samples) { [weak self] offset, done, total in + guard let self = self else { + return + } + + if let offset = offset { + self.stableTime = KronosTimeFreeze(offset: offset) + + if done == 1, let now = self.now { + first?(now, offset) + } + } + + if done == total { + completion?(self.now, offset) + } + } + } + + /// Resets all state of the monotonic clock. Note that you won't be able to access `now` until you `sync` + /// again. + func reset() { + self.stableTime = nil + } + + private func loadFromDefaults() { + guard let previousStableTime = self.storage.stableTime else { + self.stableTime = nil + return + } + self.stableTime = previousStableTime + } +} diff --git a/DatadogCore/Sources/Kronos/KronosDNSResolver.swift b/DatadogCore/Sources/Kronos/KronosDNSResolver.swift new file mode 100644 index 0000000000..5b6dfe9819 --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosDNSResolver.swift @@ -0,0 +1,108 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +private let kCopyNoOperation = unsafeBitCast(0, to: CFAllocatorCopyDescriptionCallBack.self) +private let kDefaultTimeout = 8.0 + +internal final class KronosDNSResolver { + private var completion: (([KronosInternetAddress]) -> Void)? + private var timer: Timer? + + private init() {} + + /// Performs DNS lookups and calls the given completion with the answers that are returned from the name + /// server(s) that were queried. + /// + /// - parameter host: The host to be looked up. + /// - parameter timeout: The connection timeout. + /// - parameter completion: A completion block that will be called both on failure and success with a list + /// of IPs. + static func resolve( + host: String, + timeout: TimeInterval = kDefaultTimeout, + completion: @escaping ([KronosInternetAddress]) -> Void + ) { + #if os(watchOS) + completion([]) + #else + let callback: CFHostClientCallBack = { host, _, _, info in + guard let info = info else { + return + } + let retainedSelf = Unmanaged.fromOpaque(info) + let resolver = retainedSelf.takeUnretainedValue() + resolver.timer?.invalidate() + resolver.timer = nil + + var resolved: DarwinBoolean = false + guard let addresses = CFHostGetAddressing(host, &resolved), resolved.boolValue else { + resolver.completion?([]) + retainedSelf.release() + return + } + + let IPs = (addresses.takeUnretainedValue() as NSArray) + .compactMap { $0 as? NSData } + .compactMap(KronosInternetAddress.init) + .filter { ip in !ip.isPrivate } // to avoid querying private IPs, see: https://github.com/DataDog/dd-sdk-ios/issues/647 + + resolver.completion?(IPs) + retainedSelf.release() + } + + let resolver = KronosDNSResolver() + resolver.completion = completion + + let retainedClosure = Unmanaged.passRetained(resolver).toOpaque() + var clientContext = CFHostClientContext( + version: 0, + info: UnsafeMutableRawPointer(retainedClosure), + retain: nil, + release: nil, + copyDescription: kCopyNoOperation + ) + + let hostReference = CFHostCreateWithName(kCFAllocatorDefault, host as CFString).takeUnretainedValue() + resolver.timer = Timer.scheduledTimer( + timeInterval: timeout, + target: resolver, + selector: #selector(KronosDNSResolver.onTimeout), + userInfo: hostReference, + repeats: false + ) + + CFHostSetClient(hostReference, callback, &clientContext) + CFHostScheduleWithRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) + CFHostStartInfoResolution(hostReference, .addresses, nil) + #endif + } + + #if !os(watchOS) + @objc + private func onTimeout() { + defer { + self.completion?([]) + + // Manually release the previously retained self. + Unmanaged.passUnretained(self).release() + } + + guard let userInfo = self.timer?.userInfo else { + return + } + + let hostReference = unsafeBitCast(userInfo as AnyObject, to: CFHost.self) + CFHostCancelInfoResolution(hostReference, .addresses) + CFHostUnscheduleFromRunLoop(hostReference, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue) + CFHostSetClient(hostReference, nil, nil) + } + #endif +} diff --git a/DatadogCore/Sources/Kronos/KronosData+Bytes.swift b/DatadogCore/Sources/Kronos/KronosData+Bytes.swift new file mode 100644 index 0000000000..2de3c91aa2 --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosData+Bytes.swift @@ -0,0 +1,99 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +extension Data { + /// Creates an Data instance based on a hex string (example: "ffff" would be ). + /// + /// - parameter hex: The hex string without any spaces; should only have [0-9A-Fa-f]. + init?(hex: String) { + if hex.count % 2 != 0 { + return nil + } + + let hexArray = Array(hex) + var bytes: [UInt8] = [] + + for index in stride(from: 0, to: hexArray.count, by: 2) { + guard let byte = UInt8("\(hexArray[index])\(hexArray[index + 1])", radix: 16) else { + return nil + } + + bytes.append(byte) + } + + self.init(bytes: bytes, count: bytes.count) + } + + /// Gets one byte from the given index. + /// + /// - parameter index: The index of the byte to be retrieved. Note that this should never be >= length. + /// + /// - returns: The byte located at position `index`. + func getByte(at index: Int) -> Int8 { + let data: Int8 = self.subdata(in: index ..< (index + 1)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: Int8.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return data + } + + /// Gets an unsigned int (32 bits => 4 bytes) from the given index. + /// + /// - parameter index: The index of the uint to be retrieved. Note that this should never be >= length - + /// 3. + /// + /// - returns: The unsigned int located at position `index`. + func getUnsignedInteger(at index: Int, bigEndian: Bool = true) -> UInt32 { + let data: UInt32 = self.subdata(in: index ..< (index + 4)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: UInt32.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return bigEndian ? data.bigEndian : data.littleEndian + } + + /// Gets an unsigned long integer (64 bits => 8 bytes) from the given index. + /// + /// - parameter index: The index of the ulong to be retrieved. Note that this should never be >= length - + /// 7. + /// + /// - returns: The unsigned long integer located at position `index`. + func getUnsignedLong(at index: Int, bigEndian: Bool = true) -> UInt64 { + let data: UInt64 = self.subdata(in: index ..< (index + 8)).withUnsafeBytes { rawPointer in + rawPointer.bindMemory(to: UInt64.self).baseAddress!.pointee // swiftlint:disable:this force_unwrapping + } + + return bigEndian ? data.bigEndian : data.littleEndian + } + + /// Appends the given byte (8 bits) into the receiver Data. + /// + /// - parameter data: The byte to be appended. + mutating func append(byte data: Int8) { + var data = data + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } + + /// Appends the given unsigned integer (32 bits; 4 bytes) into the receiver Data. + /// + /// - parameter data: The unsigned integer to be appended. + mutating func append(unsignedInteger data: UInt32, bigEndian: Bool = true) { + var data = bigEndian ? data.bigEndian : data.littleEndian + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } + + /// Appends the given unsigned long (64 bits; 8 bytes) into the receiver Data. + /// + /// - parameter data: The unsigned long to be appended. + mutating func append(unsignedLong data: UInt64, bigEndian: Bool = true) { + var data = bigEndian ? data.bigEndian : data.littleEndian + self.append(Data(bytes: &data, count: MemoryLayout.size)) + } +} diff --git a/DatadogCore/Sources/Kronos/KronosInternetAddress.swift b/DatadogCore/Sources/Kronos/KronosInternetAddress.swift new file mode 100644 index 0000000000..bdbf8d672f --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosInternetAddress.swift @@ -0,0 +1,163 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// This enum represents an internet address that can either be IPv4 or IPv6. +/// +/// - IPv6: An Internet Address of type IPv6 (e.g.: '::1'). +/// - IPv4: An Internet Address of type IPv4 (e.g.: '127.0.0.1'). +internal enum KronosInternetAddress: Hashable { + case ipv6(sockaddr_in6) + case ipv4(sockaddr_in) + + /// Human readable host represetnation (e.g. '192.168.1.1' or 'ab:ab:ab:ab:ab:ab:ab:ab'). + var host: String? { + switch self { + case .ipv6(var address): + var buffer = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN)) + inet_ntop(AF_INET6, &address.sin6_addr, &buffer, socklen_t(INET6_ADDRSTRLEN)) + return String(cString: buffer) + + case .ipv4(var address): + var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN)) + inet_ntop(AF_INET, &address.sin_addr, &buffer, socklen_t(INET_ADDRSTRLEN)) + return String(cString: buffer) + } + } + + /// The protocol family that should be used on the socket creation for this address. + var family: Int32 { + switch self { + case .ipv4: + return PF_INET + + case .ipv6: + return PF_INET6 + } + } + + /// If the address is reserved for private internets (local / private IP). + var isPrivate: Bool { + guard let host = host else { + return false + } + + switch self { + case .ipv6: + // Ref.: https://datatracker.ietf.org/doc/html/rfc4193#section-3 + // +--------+-+------------+-----------+----------------------------+ + // | 7 bits |1| 40 bits | 16 bits | 64 bits | + // +--------+-+------------+-----------+----------------------------+ + // | Prefix |L| Global ID | Subnet ID | Interface ID | + // +--------+-+------------+-----------+----------------------------+ + // + // Local IP is expected to have FC00::/7 prefix (7 bits) and L byte set to 1, + // which effectively means `fd` prefix for local IPs. + let localPrefix = "fd" + + // Ref.: https://datatracker.ietf.org/doc/html/rfc4291#section-2.4 + let multicastPrefix = "ff" + + let hostLowercased = host.lowercased() + return hostLowercased.starts(with: localPrefix) + || hostLowercased.starts(with: multicastPrefix) + case .ipv4: + // Ref.: https://datatracker.ietf.org/doc/html/rfc1918#section-3 + // Local IPs have predefined ranges: + // - class A: 10.0.0.0 — 10.255.255.255 + // - class B: 172.16.0.0 — 172.31.255.255 + // - class C: 192.168.0.0 — 192.168.255.255 + let classABCregex = #"^((10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(192\.168\.))"# + + // Ref.: https://datatracker.ietf.org/doc/html/rfc5771#section-3 + // Multicast address (range 224.0.0.0 - 239.255.255.255) are considered to be local network + // addresses too (https://developer.apple.com/forums/thread/663848) + // + let multicastRegex = #"^((22[4-9]\.)|(23[0-9]\.))"# + let broadcastIP = "255.255.255.255" + + return host.range(of: classABCregex, options: .regularExpression) != nil + || host.range(of: multicastRegex, options: .regularExpression) != nil + || host == broadcastIP + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.host) + } + + init?(dataWithSockAddress data: NSData) { + let storage = sockaddr_storage.from(unsafeDataWithSockAddress: data) + switch Int32(storage.ss_family) { + case AF_INET: + self = storage.withUnsafeAddress { KronosInternetAddress.ipv4($0.pointee) } + + case AF_INET6: + self = storage.withUnsafeAddress { KronosInternetAddress.ipv6($0.pointee) } + + default: + return nil + } + } + + /// Returns the address struct (either sockaddr_in or sockaddr_in6) represented as an CFData. + /// + /// - parameter port: The port number to associate on the address struct. + /// + /// - returns: An address struct wrapped into a CFData type. + func addressData(withPort port: Int) -> CFData { + switch self { + case .ipv6(var address): + address.sin6_port = in_port_t(port).bigEndian + return Data(bytes: &address, count: MemoryLayout.size) as CFData + + case .ipv4(var address): + address.sin_port = in_port_t(port).bigEndian + return Data(bytes: &address, count: MemoryLayout.size) as CFData + } + } +} + +/// Compare InternetAddress(es) by making sure the host representation are equal. +internal func == (lhs: KronosInternetAddress, rhs: KronosInternetAddress) -> Bool { + return lhs.host == rhs.host +} + +// MARK: - sockaddr_storage helpers + +extension sockaddr_storage { + /// Creates a new storage value from a data type that contains the memory layout of a sockaddr_t. This + /// is used to create sockaddr_storage(s) from some of the CF C functions such as `CFHostGetAddressing`. + /// + /// !!! WARNING: This method is unsafe and assumes the memory layout is of `sockaddr_t`. !!! + /// + /// - parameter data: The data to be interpreted as sockaddr + /// - returns: The newly created sockaddr_storage value + fileprivate static func from(unsafeDataWithSockAddress data: NSData) -> sockaddr_storage { + var storage = sockaddr_storage() + data.getBytes(&storage, length: data.length) + return storage + } + + /// Calls a closure with traditional BSD Sockets address parameters. + /// + /// - parameter body: A closure to call with `self` referenced appropriately for calling + /// BSD Sockets APIs that take an address. + /// + /// - throws: Any error thrown by `body`. + /// + /// - returns: Any result returned by `body`. + fileprivate func withUnsafeAddress(_ body: (_ address: UnsafePointer) -> T) -> T { + var storage = self + return withUnsafePointer(to: &storage) { + $0.withMemoryRebound(to: U.self, capacity: 1) { address in body(address) } + } + } +} diff --git a/DatadogCore/Sources/Kronos/KronosNSTimer+ClosureKit.swift b/DatadogCore/Sources/Kronos/KronosNSTimer+ClosureKit.swift new file mode 100644 index 0000000000..6d8faf9815 --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosNSTimer+ClosureKit.swift @@ -0,0 +1,66 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +internal typealias CKTimerHandler = (Timer) -> Void + +/// Simple closure implementation on NSTimer scheduling. +/// +/// Example: +/// +/// ```swift +/// KronosBlockTimer.scheduledTimer(withTimeInterval: 1.0) { timer in +/// print("Did something after 1s!") +/// } +/// ``` +internal final class KronosBlockTimer: NSObject { + /// Creates and returns a block-based NSTimer object and schedules it on the current run loop. + /// + /// - parameter interval: The number of seconds between firings of the timer. + /// - parameter repeated: If true, the timer will repeatedly reschedule itself until invalidated. If + /// false, the timer will be invalidated after it fires. + /// - parameter handler: The closure that the NSTimer fires. + /// + /// - returns: A new NSTimer object, configured according to the specified parameters. + class func scheduledTimer( + withTimeInterval interval: TimeInterval, + repeated: Bool = false, + handler: @escaping CKTimerHandler + ) -> Timer { + return Timer.scheduledTimer( + timeInterval: interval, + target: self, + selector: #selector(KronosBlockTimer.invokeFrom(timer:)), + userInfo: TimerClosureWrapper(handler: handler, repeats: repeated), + repeats: repeated + ) + } + + // MARK: Private methods + + @objc + private class func invokeFrom(timer: Timer) { + if let closureWrapper = timer.userInfo as? TimerClosureWrapper { + closureWrapper.handler(timer) + } + } +} + +// MARK: - Private classes + +private final class TimerClosureWrapper { + fileprivate var handler: CKTimerHandler + private var repeats: Bool + + init(handler: @escaping CKTimerHandler, repeats: Bool) { + self.handler = handler + self.repeats = repeats + } +} diff --git a/DatadogCore/Sources/Kronos/KronosNTPClient.swift b/DatadogCore/Sources/Kronos/KronosNTPClient.swift new file mode 100644 index 0000000000..17e9323c3e --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosNTPClient.swift @@ -0,0 +1,203 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +internal let kronosDefaultTimeout = 6.0 +private let kDefaultSamples = 4 +private let kMaximumNTPServers = 5 +private let kMaximumResultDispersion = 10.0 + +private typealias ObjCCompletionType = @convention(block) (Data?, TimeInterval) -> Void + +/// Exception raised while sending / receiving NTP packets. +internal enum KronosNTPNetworkError: Error { + case noValidNTPPacketFound +} + +/// NTP client session. +internal final class KronosNTPClient { + /// Query the all ips that resolve from the given pool. + /// + /// - parameter pool: NTP pool that will be resolved into multiple NTP servers. + /// - parameter port: Server NTP port (default 123). + /// - parameter version: NTP version to use (default 3). + /// - parameter numberOfSamples: The number of samples to be acquired from each server (default 4). + /// - parameter maximumServers: The maximum number of servers to be queried (default 5). + /// - parameter timeout: The individual timeout for each of the NTP operations. + /// - parameter completion: A closure that will be response PDU on success or nil on error. + func query( + pool: String = "time.apple.com", + version: Int8 = 3, + port: Int = 123, + numberOfSamples: Int = kDefaultSamples, + maximumServers: Int = kMaximumNTPServers, + timeout: CFTimeInterval = kronosDefaultTimeout, + progress: @escaping (TimeInterval?, Int, Int) -> Void + ) { + var servers: [KronosInternetAddress: [KronosNTPPacket]] = [:] + var completed: Int = 0 + + let queryIPAndStoreResult = { (address: KronosInternetAddress, totalQueries: Int) in + self.query(ip: address, port: port, version: version, timeout: timeout, numberOfSamples: numberOfSamples) { packet in + defer { + completed += 1 + + let responses = Array(servers.values) + progress(try? self.offset(from: responses), completed, totalQueries) + } + + guard let PDU = packet else { + return + } + + if servers[address] == nil { + servers[address] = [] + } + + servers[address]?.append(PDU) + } + } + + KronosDNSResolver.resolve(host: pool) { addresses in + if addresses.count == 0 { + return progress(nil, 0, 0) + } + + let totalServers = min(addresses.count, maximumServers) + let addressesToQuery = Array(addresses[0 ..< totalServers]) + + for address in addressesToQuery { + queryIPAndStoreResult(address, totalServers * numberOfSamples) + } + } + } + + /// Query the given NTP server for the time exchange. + /// + /// - parameter ip: Server socket address. + /// - parameter port: Server NTP port (default 123). + /// - parameter version: NTP version to use (default 3). + /// - parameter timeout: Timeout on socket operations. + /// - parameter numberOfSamples: The number of samples to be acquired from the server (default 4). + /// - parameter completion: A closure that will be response PDU on success or nil on error. + func query( + ip: KronosInternetAddress, + port: Int = 123, + version: Int8 = 3, + timeout: CFTimeInterval = kronosDefaultTimeout, + numberOfSamples: Int = kDefaultSamples, + completion: @escaping (KronosNTPPacket?) -> Void + ) { + var timer: Timer? + let bridgeCallback: ObjCCompletionType = { data, destinationTime in + defer { + // If we still have samples left; we'll keep querying the same server + if numberOfSamples > 1 { + self.query(ip: ip, port: port, version: version, timeout: timeout, numberOfSamples: numberOfSamples - 1, completion: completion) + } + } + timer?.invalidate() + guard + let data = data, let PDU = try? KronosNTPPacket(data: data, destinationTime: destinationTime), + PDU.isValidResponse() + else { + completion(nil) + return + } + + completion(PDU) + } + + let callback = unsafeBitCast(bridgeCallback, to: AnyObject.self) + let retainedCallback = Unmanaged.passRetained(callback) + let sourceAndSocket = self.sendAsyncUDPQuery( + to: ip, port: port, timeout: timeout, completion: UnsafeMutableRawPointer(retainedCallback.toOpaque()) + ) + + timer = KronosBlockTimer.scheduledTimer(withTimeInterval: timeout, repeated: true) { _ in + bridgeCallback(nil, TimeInterval.infinity) + retainedCallback.release() + + if let (source, socket) = sourceAndSocket { + CFSocketInvalidate(socket) + CFRunLoopRemoveSource(CFRunLoopGetMain(), source, CFRunLoopMode.commonModes) + } + } + } + + // MARK: - Private helpers (NTP Calculation) + + private func offset(from responses: [[KronosNTPPacket]]) throws -> TimeInterval { + let now = kronosCurrentTime() + var bestResponses: [KronosNTPPacket] = [] + for serverResponses in responses { + let filtered = serverResponses + .filter { abs($0.originTime - now) < kMaximumResultDispersion } + .min { $0.delay < $1.delay } + + if let filtered = filtered { + bestResponses.append(filtered) + } + } + + if bestResponses.count == 0 { + throw KronosNTPNetworkError.noValidNTPPacketFound + } + + bestResponses.sort { $0.offset < $1.offset } + return bestResponses[bestResponses.count / 2].offset + } + + // MARK: - Private helpers (CFSocket) + + private func sendAsyncUDPQuery( + to ip: KronosInternetAddress, + port: Int, + timeout: TimeInterval, + completion: UnsafeMutableRawPointer + ) -> (CFRunLoopSource, CFSocket)? { + signal(SIGPIPE, SIG_IGN) + + let callback: CFSocketCallBack = { socket, callbackType, _, data, info in + if callbackType == .writeCallBack { + var packet = KronosNTPPacket() + let PDU = packet.prepareToSend() as CFData + CFSocketSendData(socket, nil, PDU, kronosDefaultTimeout) + return + } + + guard let info = info else { + return + } + + CFSocketInvalidate(socket) + + let destinationTime = kronosCurrentTime() + let retainedClosure = Unmanaged.fromOpaque(info) + let completion = unsafeBitCast(retainedClosure.takeUnretainedValue(), to: ObjCCompletionType.self) + + let data = unsafeBitCast(data, to: CFData.self) as Data? + completion(data, destinationTime) + retainedClosure.release() + } + + let types = CFSocketCallBackType.dataCallBack.rawValue | CFSocketCallBackType.writeCallBack.rawValue + var context = CFSocketContext(version: 0, info: completion, retain: nil, release: nil, copyDescription: nil) + guard let socket = CFSocketCreate(nil, ip.family, SOCK_DGRAM, IPPROTO_UDP, types, callback, &context), + CFSocketIsValid(socket) else { + return nil + } + + let runLoopSource = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0) + CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, CFRunLoopMode.commonModes) + CFSocketConnectToAddress(socket, ip.addressData(withPort: port), timeout) + return (runLoopSource!, socket) // swiftlint:disable:this force_unwrapping + } +} diff --git a/DatadogCore/Sources/Kronos/KronosNTPPacket.swift b/DatadogCore/Sources/Kronos/KronosNTPPacket.swift new file mode 100644 index 0000000000..eda4bf998c --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosNTPPacket.swift @@ -0,0 +1,207 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Delta between system and NTP time +private let kEpochDelta = 2_208_988_800.0 + +/// This is the maximum that we'll tolerate for the client's time vs self.delay +private let kMaximumDelayDifference = 0.1 +private let kMaximumDispersion = 100.0 + +/// Returns the current time in decimal EPOCH timestamp format. +/// +/// - returns: The current time in EPOCH timestamp format. +internal func kronosCurrentTime() -> TimeInterval { + var current = timeval() + let systemTimeError = gettimeofday(¤t, nil) != 0 + assert(!systemTimeError, "system clock error: system time unavailable") + + return Double(current.tv_sec) + Double(current.tv_usec) / 1_000_000 +} + +internal struct KronosNTPPacket { + /// The leap indicator warning of an impending leap second to be inserted or deleted in the last + /// minute of the current month. + let leap: KronosLeapIndicator + + /// Version Number (VN): This is a three-bit integer indicating the NTP version number, currently 3. + let version: Int8 + + /// The current connection mode. + let mode: KronosMode + + /// Mode representing the stratum level of the local clock. + let stratum: KronosStratum + + /// Indicates the maximum interval between successive messages, in seconds to the nearest power of two. + /// The values that normally appear in this field range from 6 to 10, inclusive. + let poll: Int8 + + /// The precision of the local clock, in seconds to the nearest power of two. The values that normally + /// appear in this field range from -6 for mains-frequency clocks to -18 for microsecond clocks found + /// in some workstations. + let precision: Int8 + + /// The total roundtrip delay to the primary reference source, in seconds with fraction point between + /// bits 15 and 16. Note that this variable can take on both positive and negative values, depending on + /// the relative time and frequency errors. The values that normally appear in this field range from + /// negative values of a few milliseconds to positive values of several hundred milliseconds. + let rootDelay: TimeInterval + + /// Total dispersion to the reference clock, in EPOCH. + let rootDispersion: TimeInterval + + /// Server or reference clock. This value is generated based on a reference identifier maintained by IANA. + let clockSource: KronosClockSource + + /// Time when the system clock was last set or corrected, in EPOCH timestamp format. + let referenceTime: TimeInterval + + /// Time at the client when the request departed for the server, in EPOCH timestamp format. + let originTime: TimeInterval + + /// Time at the server when the request arrived from the client, in EPOCH timestamp format. + let receiveTime: TimeInterval + + /// Time at the server when the response left for the client, in EPOCH timestamp format. + var transmitTime: TimeInterval = 0.0 + + /// Time at the client when the response arrived, in EPOCH timestamp format. + let destinationTime: TimeInterval + + /// NTP protocol package representation. + /// + /// - parameter transmitTime: Packet transmission timestamp. + /// - parameter version: NTP protocol version. + /// - parameter mode: Packet mode (client, server). + init(version: Int8 = 3, mode: KronosMode = .client) { + self.version = version + self.leap = .noWarning + self.mode = mode + self.stratum = .unspecified + self.poll = 4 + self.precision = -6 + self.rootDelay = 1 + self.rootDispersion = 1 + self.clockSource = .referenceIdentifier(id: 0) + self.referenceTime = -kEpochDelta + self.originTime = -kEpochDelta + self.receiveTime = -kEpochDelta + self.destinationTime = -1 + } + + /// Creates a NTP package based on a network PDU. + /// + /// - parameter data: The PDU received from the NTP call. + /// - parameter destinationTime: The time where the package arrived (client time) in EPOCH format. + /// - throws: KronosNTPParsingError in case of an invalid response. + init(data: Data, destinationTime: TimeInterval) throws { + if data.count < 48 { + throw KronosNTPParsingError.invalidNTPPDU("Invalid PDU length: \(data.count)") + } + + self.leap = KronosLeapIndicator(rawValue: (data.getByte(at: 0) >> 6) & 0b11) ?? .noWarning + self.version = data.getByte(at: 0) >> 3 & 0b111 + self.mode = KronosMode(rawValue: data.getByte(at: 0) & 0b111) ?? .unknown + self.stratum = KronosStratum(value: data.getByte(at: 1)) + self.poll = data.getByte(at: 2) + self.precision = data.getByte(at: 3) + self.rootDelay = KronosNTPPacket.intervalFromNTPFormat(data.getUnsignedInteger(at: 4)) + self.rootDispersion = KronosNTPPacket.intervalFromNTPFormat(data.getUnsignedInteger(at: 8)) + self.clockSource = KronosClockSource(stratum: self.stratum, sourceID: data.getUnsignedInteger(at: 12)) + self.referenceTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 16)) + self.originTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 24)) + self.receiveTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 32)) + self.transmitTime = KronosNTPPacket.dateFromNTPFormat(data.getUnsignedLong(at: 40)) + self.destinationTime = destinationTime + } + + /// Convert this NTPPacket to a buffer that can be sent over a socket. + /// + /// - returns: A bytes buffer representing this packet. + mutating func prepareToSend(transmitTime: TimeInterval? = nil) -> Data { + var data = Data() + data.append(byte: self.leap.rawValue << 6 | self.version << 3 | self.mode.rawValue) + data.append(byte: self.stratum.rawValue) + data.append(byte: self.poll) + data.append(byte: self.precision) + data.append(unsignedInteger: self.intervalToNTPFormat(self.rootDelay)) + data.append(unsignedInteger: self.intervalToNTPFormat(self.rootDispersion)) + data.append(unsignedInteger: self.clockSource.ID) + data.append(unsignedLong: self.dateToNTPFormat(self.referenceTime)) + data.append(unsignedLong: self.dateToNTPFormat(self.originTime)) + data.append(unsignedLong: self.dateToNTPFormat(self.receiveTime)) + + self.transmitTime = transmitTime ?? kronosCurrentTime() + data.append(unsignedLong: self.dateToNTPFormat(self.transmitTime)) + return data + } + + /// Checks properties to make sure that the received PDU is a valid response that we can use. + /// + /// - returns: A boolean indicating if the response is valid for the given version. + func isValidResponse() -> Bool { + return (self.mode == .server || self.mode == .symmetricPassive) && self.leap != .alarm + && self.stratum != .invalid && self.stratum != .unspecified + && self.rootDispersion < kMaximumDispersion + && abs(kronosCurrentTime() - self.originTime - self.delay) < kMaximumDelayDifference + } + + // MARK: - Private helpers + + private func dateToNTPFormat(_ time: TimeInterval) -> UInt64 { + let integer = UInt32(time + kEpochDelta) + let decimal = modf(time).1 * 4_294_967_296.0 // 2 ^ 32 + return UInt64(integer) << 32 | UInt64(decimal) + } + + private func intervalToNTPFormat(_ time: TimeInterval) -> UInt32 { + let integer = UInt16(time) + let decimal = modf(time).1 * 65_536 // 2 ^ 16 + return UInt32(integer) << 16 | UInt32(decimal) + } + + private static func dateFromNTPFormat(_ time: UInt64) -> TimeInterval { + let integer = Double(time >> 32) + let decimal = Double(time & 0xffffffff) / 4_294_967_296.0 + return integer - kEpochDelta + decimal + } + + private static func intervalFromNTPFormat(_ time: UInt32) -> TimeInterval { + let integer = Double(time >> 16) + let decimal = Double(time & 0xffff) / 65_536 + return integer + decimal + } +} + +/// From RFC 2030 (with a correction to the delay math): +/// +/// Timestamp Name ID When Generated +/// ------------------------------------------------------------ +/// Originate Timestamp T1 time request sent by client +/// Receive Timestamp T2 time request received by server +/// Transmit Timestamp T3 time reply sent by server +/// Destination Timestamp T4 time reply received by client +/// +/// The roundtrip delay d and local clock offset t are defined as +/// +/// d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2. +extension KronosNTPPacket { + /// Clocks offset in seconds. + var offset: TimeInterval { + return ((self.receiveTime - self.originTime) + (self.transmitTime - self.destinationTime)) / 2.0 + } + + /// Round-trip delay in seconds + var delay: TimeInterval { + return (self.destinationTime - self.originTime) - (self.transmitTime - self.receiveTime) + } +} diff --git a/DatadogCore/Sources/Kronos/KronosNTPProtocol.swift b/DatadogCore/Sources/Kronos/KronosNTPProtocol.swift new file mode 100644 index 0000000000..f323024f50 --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosNTPProtocol.swift @@ -0,0 +1,137 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Exception raised when the received PDU is invalid. +internal enum KronosNTPParsingError: Error { + case invalidNTPPDU(String) +} + +/// The leap indicator warning of an impending leap second to be inserted or deleted in the last minute of the +/// current month. +internal enum KronosLeapIndicator: Int8 { + case noWarning, sixtyOneSeconds, fiftyNineSeconds, alarm + + /// Human readable value of the leap warning. + var description: String { + switch self { + case .noWarning: + return "No warning" + case .sixtyOneSeconds: + return "Last minute of the day has 61 seconds" + case .fiftyNineSeconds: + return "Last minute of the day has 59 seconds" + case .alarm: + return "Unknown (clock unsynchronized)" + } + } +} + +/// The connection mode. +internal enum KronosMode: Int8 { + case reserved, symmetricActive, symmetricPassive, client, server, broadcast, reservedNTP, unknown +} + +/// Mode representing the stratum level of the clock. +internal enum KronosStratum: Int8 { + case unspecified, primary, secondary, invalid + + init(value: Int8) { + switch value { + case 0: + self = .unspecified + + case 1: + self = .primary + + case 0 ..< 15: + self = .secondary + + default: + self = .invalid + } + } +} + +/// Server or reference clock. This value is generated based on the server stratum. +/// +/// - ReferenceClock: Contains the sourceID and the description for the reference clock (stratum 1). +/// - Debug(id): Contains the kiss code for debug purposes (stratum 0). +/// - ReferenceIdentifier(id): The reference identifier of the server (stratum > 1). +internal enum KronosClockSource { + case referenceClock(id: UInt32, description: String) + case debug(id: UInt32) + case referenceIdentifier(id: UInt32) + + init(stratum: KronosStratum, sourceID: UInt32) { + switch stratum { + case .unspecified: + self = .debug(id: sourceID) + + case .primary: + let (id, description) = KronosClockSource.description(fromID: sourceID) + self = .referenceClock(id: id, description: description) + + case .secondary, .invalid: + self = .referenceIdentifier(id: sourceID) + } + } + + /// The id for the reference clock (IANA, stratum 1), debug (stratum 0) or referenceIdentifier + var ID: UInt32 { + switch self { + case .referenceClock(let id, _): + return id + + case .debug(let id): + return id + + case .referenceIdentifier(let id): + return id + } + } + + private static func description(fromID sourceID: UInt32) -> (UInt32, String) { + let sourceMap: [UInt32: String] = [ + 0x47505300: "Global Position System", + 0x47414c00: "Galileo Positioning System", + 0x50505300: "Generic pulse-per-second", + 0x49524947: "Inter-Range Instrumentation Group", + 0x57575642: "LF Radio WWVB Ft. Collins, CO 60 kHz", + 0x44434600: "LF Radio DCF77 Mainflingen, DE 77.5 kHz", + 0x48424700: "LF Radio HBG Prangins, HB 75 kHz", + 0x4d534600: "LF Radio MSF Anthorn, UK 60 kHz", + 0x4a4a5900: "LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz", + 0x4c4f5243: "MF Radio LORAN C station, 100 kHz", + 0x54444600: "MF Radio Allouis, FR 162 kHz", + 0x43485500: "HF Radio CHU Ottawa, Ontario", + 0x57575600: "HF Radio WWV Ft. Collins, CO", + 0x57575648: "HF Radio WWVH Kauai, HI", + 0x4e495354: "NIST telephone modem", + 0x41435453: "ACTS telephone modem", + 0x55534e4f: "USNO telephone modem", + 0x50544200: "European telephone modem", + 0x4c4f434c: "Uncalibrated local clock", + 0x4345534d: "Calibrated Cesium clock", + 0x5242444d: "Calibrated Rubidium clock", + 0x4f4d4547: "OMEGA radio navigation system", + 0x44434e00: "DCN routing protocol", + 0x54535000: "TSP time protocol", + 0x44545300: "Digital Time Service", + 0x41544f4d: "Atomic clock (calibrated)", + 0x564c4600: "VLF radio (OMEGA,, etc.)", + 0x31505053: "External 1 PPS input", + 0x46524545: "(Internal clock)", + 0x494e4954: "(Initialization)", + ] + + return (sourceID, sourceMap[sourceID] ?? "NULL") + } +} diff --git a/DatadogCore/Sources/Kronos/KronosTimeFreeze.swift b/DatadogCore/Sources/Kronos/KronosTimeFreeze.swift new file mode 100644 index 0000000000..b559742f4a --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosTimeFreeze.swift @@ -0,0 +1,96 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +private let kUptimeKey = "Uptime" +private let kTimestampKey = "Timestamp" +private let kOffsetKey = "Offset" + +internal struct KronosTimeFreeze { + private let uptime: TimeInterval + private let timestamp: TimeInterval + private let offset: TimeInterval + + /// The stable timestamp adjusted by the most accurate offset known so far. + var adjustedTimestamp: TimeInterval { + return self.offset + self.stableTimestamp + } + + /// The stable timestamp (calculated based on the uptime); note that this doesn't have sub-seconds + /// precision. See `getSystemUptime()` for more information. + var stableTimestamp: TimeInterval { + return (KronosTimeFreeze.getSystemUptime() - self.uptime) + self.timestamp + } + + /// Time interval between now and the time the NTP response represented by this TimeFreeze was received. + var timeSinceLastNtpSync: TimeInterval { + return KronosTimeFreeze.getSystemUptime() - uptime + } + + init(offset: TimeInterval) { + self.offset = offset + self.timestamp = kronosCurrentTime() + self.uptime = KronosTimeFreeze.getSystemUptime() + } + + init?(from dictionary: [String: TimeInterval]) { + guard let uptime = dictionary[kUptimeKey], + let timestamp = dictionary[kTimestampKey], + let offset = dictionary[kOffsetKey] else { + return nil + } + + let currentUptime = KronosTimeFreeze.getSystemUptime() + let currentTimestamp = kronosCurrentTime() + let currentBoot = currentUptime - currentTimestamp + let previousBoot = uptime - timestamp + if rint(currentBoot) - rint(previousBoot) != 0 { + return nil + } + + self.uptime = uptime + self.timestamp = timestamp + self.offset = offset + } + + /// Convert this TimeFreeze to a dictionary representation. + /// + /// - returns: A dictionary representation. + func toDictionary() -> [String: TimeInterval] { + return [ + kUptimeKey: self.uptime, + kTimestampKey: self.timestamp, + kOffsetKey: self.offset, + ] + } + + /// Returns a high-resolution measurement of system uptime, that continues ticking through device sleep + /// *and* user- or system-generated clock adjustments. This allows for stable differences to be calculated + /// between timestamps. + /// + /// Note: Due to an issue in BSD/darwin, sub-second precision will be lost; + /// see: https://github.com/darwin-on-arm/xnu/blob/master/osfmk/kern/clock.c#L522. + /// + /// - returns: An Int measurement of system uptime in microseconds. + private static func getSystemUptime() -> TimeInterval { + var mib = [CTL_KERN, KERN_BOOTTIME] + var size = MemoryLayout.stride + var bootTime = timeval() + + let bootTimeError = sysctl(&mib, u_int(mib.count), &bootTime, &size, nil, 0) != 0 + assert(!bootTimeError, "system clock error: kernel boot time unavailable") + + let now = kronosCurrentTime() + let uptime = Double(bootTime.tv_sec) + Double(bootTime.tv_usec) / 1_000_000 + assert(now >= uptime, "inconsistent clock state: system time precedes boot time") + + return now - uptime + } +} diff --git a/DatadogCore/Sources/Kronos/KronosTimeStorage.swift b/DatadogCore/Sources/Kronos/KronosTimeStorage.swift new file mode 100644 index 0000000000..54418c27cd --- /dev/null +++ b/DatadogCore/Sources/Kronos/KronosTimeStorage.swift @@ -0,0 +1,71 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import Foundation + +/// Defines where the user defaults are stored +internal enum KronosTimeStoragePolicy { + /// Uses `UserDefaults.Standard` + case standard + /// Attempts to use the specified App Group ID (which is the String) to access shared storage. + case appGroup(String) + + /// Creates an instance + /// + /// - parameter appGroupID: The App Group ID that maps to a shared container for `UserDefaults`. If this + /// is nil, the resulting instance will be `.standard` + init(appGroupID: String?) { + if let appGroupID = appGroupID { + self = .appGroup(appGroupID) + } else { + self = .standard + } + } +} + +/// Handles saving and retrieving instances of `KronosTimeFreeze` for quick retrieval +internal struct KronosTimeStorage { + private var userDefaults: UserDefaults // swiftlint:disable:this required_reason_api_name + private let kDefaultsKey = "KronosStableTime" + + /// The most recent stored `TimeFreeze`. Getting retrieves from the UserDefaults defined by the storage + /// policy. Setting sets the value in UserDefaults + var stableTime: KronosTimeFreeze? { + get { + guard let stored = self.userDefaults.value(forKey: kDefaultsKey) as? [String: TimeInterval], + let previousStableTime = KronosTimeFreeze(from: stored) else { + return nil + } + + return previousStableTime + } + + set { + guard let newFreeze = newValue else { + return + } + + self.userDefaults.set(newFreeze.toDictionary(), forKey: kDefaultsKey) + } + } + + /// Creates an instance + /// + /// - parameter storagePolicy: Defines the storage location of `UserDefaults` + init(storagePolicy: KronosTimeStoragePolicy) { + switch storagePolicy { + case .standard: + self.userDefaults = .standard + case .appGroup(let groupName): + let sharedDefaults = UserDefaults(suiteName: groupName) // swiftlint:disable:this required_reason_api_name + assert(sharedDefaults != nil, "Could not create UserDefaults for group: '\(groupName)'") + self.userDefaults = sharedDefaults ?? .standard + } + } +} diff --git a/DatadogCore/Sources/PerformancePreset.swift b/DatadogCore/Sources/PerformancePreset.swift new file mode 100644 index 0000000000..ef4914fbac --- /dev/null +++ b/DatadogCore/Sources/PerformancePreset.swift @@ -0,0 +1,153 @@ +/* + * 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 + +internal protocol StoragePerformancePreset { + /// Maximum size of a single file (in bytes). + /// Each feature (logging, tracing, ...) serializes its objects data to that file for later upload. + /// If last written file is too big to append next data, new file is created. + var maxFileSize: UInt64 { get } + /// Maximum size of data directory (in bytes). + /// Each feature uses separate directory. + /// If this size is exceeded, the oldest files are deleted until this limit is met again. + var maxDirectorySize: UInt64 { get } + /// Maximum age qualifying given file for reuse (in seconds). + /// If recently used file is younger than this, it is reused - otherwise: new file is created. + var maxFileAgeForWrite: TimeInterval { get } + /// Minimum age qualifying given file for upload (in seconds). + /// If the file is older than this, it is uploaded (and then deleted if upload succeeded). + /// It has an arbitrary offset (~0.5s) over `maxFileAgeForWrite` to ensure that no upload can start for the file being currently written. + var minFileAgeForRead: TimeInterval { get } + /// Maximum age qualifying given file for upload (in seconds). + /// Files older than this are considered obsolete and get deleted without uploading. + var maxFileAgeForRead: TimeInterval { get } + /// Maximum number of serialized objects written to a single file. + /// If number of objects in recently used file reaches this limit, new file is created for new data. + var maxObjectsInFile: Int { get } + /// Maximum size of serialized object data (in bytes). + /// If serialized object data exceeds this limit, it is skipped (not written to file and not uploaded). + var maxObjectSize: UInt64 { get } +} + +internal extension StoragePerformancePreset { + /// The uploader window duration determines when a file is considered "ready for upload" by the uploader after the last write. + /// + /// This value is crucial for computing batching and upload metrics within the SDK (see RUMM-3459). The uploader window is derived from the + /// original `batchSize` value, which is either configured by the user or set as an internal override. The `batchSize` represents the age of + /// "batch maturity" for uploads and is specified in seconds. + /// + /// The uploader window is calculated as the average of two key parameters: `minFileAgeForRead` and `maxFileAgeForWrite`. Batches younger + /// than `maxFileAgeForWrite` are considered "writable" (available for the writer), while batches older than `minFileAgeForRead` are + /// meant to be "readable" (available for the uploader). To ensure that the writer and uploader don't access the same batch simultaneously, + /// a safe-guard window (10% of `batchSize`) is implemented within which the batch is neither writable nor readable. + var uploaderWindow: TimeInterval { (minFileAgeForRead + maxFileAgeForWrite) * 0.5 } +} + +internal struct PerformancePreset: Equatable, StoragePerformancePreset, UploadPerformancePreset { + // MARK: - StoragePerformancePreset + + let maxFileSize: UInt64 + let maxDirectorySize: UInt64 + let maxFileAgeForWrite: TimeInterval + let minFileAgeForRead: TimeInterval + let maxFileAgeForRead: TimeInterval + let maxObjectsInFile: Int + let maxObjectSize: UInt64 + + // MARK: - UploadPerformancePreset + + let initialUploadDelay: TimeInterval + let minUploadDelay: TimeInterval + let maxUploadDelay: TimeInterval + let uploadDelayChangeRate: Double +} + +internal extension PerformancePreset { + init( + batchSize: Datadog.Configuration.BatchSize, + uploadFrequency: Datadog.Configuration.UploadFrequency, + bundleType: BundleType + ) { + let meanFileAgeInSeconds: TimeInterval = { + switch (bundleType, batchSize) { + case (.iOSApp, .small): return 3 + case (.iOSApp, .medium): return 10 + case (.iOSApp, .large): return 35 + case (.iOSAppExtension, _): return 1 + } + }() + + let minUploadDelayInSeconds: TimeInterval = { + switch (bundleType, uploadFrequency) { + case (.iOSApp, .frequent): return 0.5 + case (.iOSApp, .average): return 2 + case (.iOSApp, .rare): return 5 + case (.iOSAppExtension, _): return 0.5 + } + }() + + let uploadDelayFactors: (initial: Double, min: Double, max: Double, changeRate: Double) = { + switch bundleType { + case .iOSApp: + return ( + initial: 5, + min: 1, + max: 10, + changeRate: 0.1 + ) + case .iOSAppExtension: + return ( + initial: 0.5, // ensures the the first upload is checked quickly after starting the short-lived app extension + min: 1, + max: 5, + changeRate: 0.5 // if batches are found, reduces interval significantly for more uploads in short-lived app extension + ) + } + }() + + self.init( + meanFileAge: meanFileAgeInSeconds, + minUploadDelay: minUploadDelayInSeconds, + uploadDelayFactors: uploadDelayFactors + ) + } + + init( + meanFileAge: TimeInterval, + minUploadDelay: TimeInterval, + uploadDelayFactors: (initial: Double, min: Double, max: Double, changeRate: Double) + ) { + self.maxFileSize = 4.MB.asUInt64() + self.maxDirectorySize = 512.MB.asUInt64() + self.maxFileAgeForWrite = meanFileAge * 0.95 // 5% below the mean age + self.minFileAgeForRead = meanFileAge * 1.05 // 5% above the mean age + self.maxFileAgeForRead = 18.hours + self.maxObjectsInFile = 500 + self.maxObjectSize = 512.KB.asUInt64() + self.initialUploadDelay = minUploadDelay * uploadDelayFactors.initial + self.minUploadDelay = minUploadDelay * uploadDelayFactors.min + self.maxUploadDelay = minUploadDelay * uploadDelayFactors.max + self.uploadDelayChangeRate = uploadDelayFactors.changeRate + } + + func updated(with override: PerformancePresetOverride) -> PerformancePreset { + return PerformancePreset( + maxFileSize: override.maxFileSize ?? maxFileSize, + maxDirectorySize: maxDirectorySize, + maxFileAgeForWrite: override.maxFileAgeForWrite ?? maxFileAgeForWrite, + minFileAgeForRead: override.minFileAgeForRead ?? minFileAgeForRead, + maxFileAgeForRead: maxFileAgeForRead, + maxObjectsInFile: maxObjectsInFile, + maxObjectSize: override.maxObjectSize ?? maxObjectSize, + initialUploadDelay: override.initialUploadDelay ?? initialUploadDelay, + minUploadDelay: override.minUploadDelay ?? minUploadDelay, + maxUploadDelay: override.maxUploadDelay ?? maxUploadDelay, + uploadDelayChangeRate: override.uploadDelayChangeRate ?? uploadDelayChangeRate + ) + } +} diff --git a/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift new file mode 100644 index 0000000000..b7deb4ce21 --- /dev/null +++ b/DatadogCore/Sources/SDKMetrics/BatchMetrics.swift @@ -0,0 +1,129 @@ +/* + * 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 + +/// Common definitions for batch telemetries. +internal enum BatchMetric { + /// Track name key + static let trackKey = "track" + /// Track name value. + /// Returns the corresponding track value based on the given feature name. + static func trackValue(for featureName: String) -> String? { + switch featureName { + case "rum": return "rum" + case "logging": return "logs" + case "tracing": return "trace" + case "session-replay": return "sr" + case "session-replay-resources": return "sr-resources" + default: return nil + } + } + /// Consent label key. + /// It is added to differentiate telemetries for batches handled in `pending` and `granted` consents. + static let consentKey = "consent" + /// "granted" consent value. + static let consentGrantedValue = "granted" + /// "pending" consent value. + static let consentPendingValue = "pending" +} + +/// Definition of "Batch Deleted" telemetry. +internal enum BatchDeletedMetric { + /// The name of this metric, included in telemetry log. + /// Note: the "[Mobile Metric]" prefix is added when sending this telemetry in RUM. + 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). + static let uploaderDelayMinKey = "min" + /// The min delay of uploads for this track (in ms). + static let uploaderDelayMaxKey = "max" + /// 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" + + /// The duration from batch creation to batch deletion (in ms). + static let batchAgeKey = "batch_age" + /// The reason of batch deletion. + 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 { + /// The batch was delivered to Intake and deleted upon receiving given `responseCode`. + /// + /// The intake-code-202 represents a successful delivery. While some status codes, such as 401, indicate unrecoverable + /// user errors, others, like 400, will indicate faults within the SDK. It is important to note that not all status codes will appear + /// in this field, as the SDKs implement retry mechanisms for certain codes, e.g. 503 (see: ``DataUploadStatus``). + case intakeCode(responseCode: Int?) + /// The batch become obsolete (older than allowed limit for this track's intake). + case obsolete + /// The batch was deleted due to exceeding allowed max size for batches directory. + case purged + /// The feature failed to create request for that batch (e.g. data was malformed). + case invalid + /// The batch was deleted arbitrarily without considering its delivery status. This option is only used in test logic + /// and we don't send "Batch Deleted" metric for this case. + case flushed + + /// Converts the removal reason to the string value expected for `batchRemovalReasonKey`. + func toString() -> String { + switch self { + case .intakeCode(let responseCode): + return "intake-code-\(responseCode.map { String($0) } ?? "unknown")" + case .obsolete: + return "obsolete" + case .purged: + return "purged" + case .invalid: + return "invalid" + case .flushed: + return "flushed" + } + } + + /// Indicates whether the metric should be sent for this removal reason. + var includeInMetric: Bool { + switch self { + case .intakeCode, .obsolete, .purged, .invalid: + return true + case .flushed: + return false + } + } + } +} + +/// Definition of "Batch Closed" telemetry. +internal enum BatchClosedMetric { + /// The name of this metric, included in telemetry log. + /// Note: the "[Mobile Metric]" prefix is added when sending this telemetry in RUM. + 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" + + /// The size of batch at closing (in bytes). + static let batchSizeKey = "batch_size" + /// The number of events written to this batch before closing. + static let batchEventsCountKey = "batch_events_count" + /// The duration from batch creation to batch closing (in ms). + static let batchDurationKey = "batch_duration" + /// If the batch was closed by core or after new batch was forced by the feature. + static let forcedNewKey = "forced_new" +} diff --git a/DatadogCore/Sources/Utils/Cryptography.swift b/DatadogCore/Sources/Utils/Cryptography.swift new file mode 100644 index 0000000000..ee16eadb61 --- /dev/null +++ b/DatadogCore/Sources/Utils/Cryptography.swift @@ -0,0 +1,21 @@ +/* + * 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 + +/// Computes SHA256 for given `string`. +internal func sha256(_ string: String) -> String { + guard let data = string.data(using: .utf8) else { + return string + } + + var digest: [UInt8] = Array(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + _ = data.withUnsafeBytes { CC_SHA256($0.baseAddress, UInt32(data.count), &digest) } + return digest + .map({ byte in String(format: "%02x", byte) }) + .joined(separator: "") +} diff --git a/DatadogCore/Sources/Utils/Retrying.swift b/DatadogCore/Sources/Utils/Retrying.swift new file mode 100644 index 0000000000..fb4813164c --- /dev/null +++ b/DatadogCore/Sources/Utils/Retrying.swift @@ -0,0 +1,21 @@ +/* + * 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 + +/// Retries given `block` several `times` in predefined `delay` until it does not throw an error. +/// If all tries resulted with error, the last `Error` is thrown from this function. +internal func retry(times: UInt, delay: TimeInterval, block: () throws -> R) throws -> R { + for _ in (1.. Directory? { + try? temporaryCoreDirectory.coreDirectory.subdirectory(path: store.directoryPath) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift b/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift new file mode 100644 index 0000000000..3cb2b68ac4 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/DirectoriesTests.swift @@ -0,0 +1,101 @@ +/* + * 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 +@testable import DatadogCore + +class DirectoriesTests: XCTestCase { + lazy var directory = Directory(url: temporaryDirectory) + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + func testWhenCreatingCoreDirectory_thenItsNameIsUniqueForClientTokenAndSite() throws { + // Given + let fixtures: [(instancenName: String, site: DatadogSite, expectedName: String)] = [ + ("abcdef", .us1, "d5f91716d9c17bc76cb9931e1f9ff37724a27d4c05f1eb7081f59ea34d44c777"), + ("abcdef", .us3, "4a2e7e5b459af9976950e85463db2ba1e71500cdd77ead26b41559bf5a372dfb"), + ("abcdef", .us5, "38028ebbeab2aa980eab9e5a8ee714f93e8118621472697dabe084e6a9c55cd1"), + ("abcdef", .eu1, "ff203358d7d236d35dd6acbe6f74b2db17c5855c9a8c43d4f9c2d6869af413e9"), + ("abcdef", .ap1, "e7f8dbbceb3cb6c93d74a8fc6ba9c6a43c05c00b792b65b183f62edb98709c79"), + ("abcdef", .us1_fed, "2a69100a36ae68ad3b081daa4c254fcade6b804ec71eda9109b7ec4b8317940b"), + ("ghijkl", .us1, "158931c9e9576ef6ed1576721227d29e641e3f0ec2083e4bff280684f6b7ca94"), + ("ghijkl", .us3, "e098808a9b0e3695f6b876ff677e50aaf98034606369abeabd5df45bbe8bb739"), + ("ghijkl", .us5, "6212ba431e02e4da2da2f36a5fe9d26b4c33641a63be75c22e81196acfde7d91"), + ("ghijkl", .eu1, "16fbe70ae92694f96bb36021589ae2ae5f050872548c26fe320cde96eac81957"), + ("ghijkl", .ap1, "396717396bd53c4019640e9b6f6f1848f10fa95752c497d3a93de88e2600d550"), + ("ghijkl", .us1_fed, "1585291b515c607624ed20935382bde4438ffac64f190b20a064eb6c1b734c6b"), + ] + + // When + let coreDirectories = try fixtures.map { instancenName, site, _ in + try CoreDirectory( + in: directory, + instanceName: instancenName, + site: site + ) + } + defer { coreDirectories.forEach { $0.delete() } } + + // Then + zip(fixtures, coreDirectories).forEach { fixture, coreDirectory in + let directoryName = coreDirectory.coreDirectory.url.lastPathComponent + XCTAssertEqual(directoryName, fixture.expectedName) + XCTAssertFalse( + directoryName.contains(fixture.instancenName), + "The core directory name must not include client token" + ) + } + } + + func testGivenDifferentSDKConfigurations_whenCreatingCoreDirectories_thenEachDirectoryIsUnique() throws { + // When + let coreDirectories = try (0..<50).map { index in + try CoreDirectory( + in: directory, + instanceName: .mockRandom(among: .alphanumerics, length: 31) + "\(index)", + site: .mockRandom() + ) + } + defer { coreDirectories.forEach { $0.delete() } } + + // Then + let uniqueCoreDirectoryURLs = Set(coreDirectories.map({ $0.coreDirectory.url })) + XCTAssertEqual( + coreDirectories.count, + uniqueCoreDirectoryURLs.count, + "It must create unique core directory URL for each SDK configuration" + ) + } + + func testGivenCoreDirectory_whenCreatingFeatureDirectories_thenTheirPathsAreRelative() throws { + // Given + let coreDirectory = temporaryCoreDirectory.create() + defer { coreDirectory.delete() } + + // When + let featureDirectories = try coreDirectory.getFeatureDirectories(forFeatureNamed: .mockRandom()) + + // Then + XCTAssertTrue( + featureDirectories.authorized.url.path.contains(coreDirectory.coreDirectory.url.path), + "Feature's authorized directory must be relative to core directory" + ) + XCTAssertTrue( + featureDirectories.unauthorized.url.path.contains(coreDirectory.coreDirectory.url.path), + "Feature's unauthorized directory must be relative to core directory" + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/FeatureTests.swift b/DatadogCore/Tests/Datadog/Core/FeatureTests.swift new file mode 100644 index 0000000000..8e7b3e7c71 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/FeatureTests.swift @@ -0,0 +1,157 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogCore + +class FeatureStorageTests: XCTestCase { + private let queue = DispatchQueue(label: "feature-storage-test") + private var storage: FeatureStorage! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + storage = FeatureStorage( + featureName: .mockAny(), + queue: queue, + directories: temporaryFeatureDirectories, + dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), + performance: .mockRandom(), + encryption: nil, + backgroundTasksEnabled: .mockRandom(), + telemetry: NOPTelemetry() + ) + temporaryFeatureDirectories.create() + } + + override func tearDown() { + temporaryFeatureDirectories.delete() + storage = nil + super.tearDown() + } + + // MARK: - Writing data + + func testWhenWritingEventsWithoutForcingNewBatch_itShouldWriteAllEventsToTheSameBatch() throws { + // When + storage.writer(for: .granted).write(value: ["event1": "1"]) + storage.writer(for: .granted).write(value: ["event2": "2"]) + storage.writer(for: .granted).write(value: ["event3": "3"]) + + // Then + storage.setIgnoreFilesAgeWhenReading(to: true) + + let batch = try XCTUnwrap(storage.reader.readNextBatches(1).first) + XCTAssertEqual(batch.events.count, 3, "All 3 events should be written to the same batch") + storage.reader.markBatchAsRead(batch) + + XCTAssertTrue(storage.reader.readNextBatches(1).isEmpty, "There must be no other batches") + } + + // MARK: - Behaviours on tracking consent + + func testWhenWritingEventsInDifferentConsents_itOnlyReadsGrantedEvents() throws { + // When + storage.writer(for: .granted).write(value: ["event.consent": "granted"]) + storage.writer(for: .pending).write(value: ["event.consent": "pending"]) + storage.writer(for: .notGranted).write(value: ["event.consent": "notGranted"]) + + // Then + storage.setIgnoreFilesAgeWhenReading(to: true) + + let batch = try XCTUnwrap(storage.reader.readNextBatches(1).first) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) + storage.reader.markBatchAsRead(batch) + + XCTAssertTrue(storage.reader.readNextBatches(1).isEmpty, "There must be no other batches") + } + + func testGivenEventsWrittenInDifferentConsents_whenChangingConsentToGranted_itMakesPendingEventsReadable() throws { + // Given + storage.writer(for: .granted).write(value: ["event.consent": "granted"]) + storage.writer(for: .pending).write(value: ["event.consent": "pending"]) + storage.writer(for: .notGranted).write(value: ["event.consent": "notGranted"]) + + // When + storage.migrateUnauthorizedData(toConsent: .granted) + + // Then + storage.setIgnoreFilesAgeWhenReading(to: true) + + var batch = try XCTUnwrap(storage.reader.readNextBatches(1).first) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) + storage.reader.markBatchAsRead(batch) + + batch = try XCTUnwrap(storage.reader.readNextBatches(1).first) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"pending"}"#]) + storage.reader.markBatchAsRead(batch) + + XCTAssertTrue(storage.reader.readNextBatches(1).isEmpty, "There must be no other batches") + } + + func testGivenEventsWrittenInDifferentConsents_whenChangingConsentToNotGranted_itDeletesPendingEvents() throws { + // Given + storage.writer(for: .granted).write(value: ["event.consent": "granted"]) + storage.writer(for: .pending).write(value: ["event.consent": "pending"]) + storage.writer(for: .notGranted).write(value: ["event.consent": "notGranted"]) + + // When + storage.migrateUnauthorizedData(toConsent: .notGranted) + + // Then + storage.setIgnoreFilesAgeWhenReading(to: true) + + let batch = try XCTUnwrap(storage.reader.readNextBatches(1).first) + XCTAssertEqual(batch.events.map { $0.data.utf8String }, [#"{"event.consent":"granted"}"#]) + storage.reader.markBatchAsRead(batch) + + XCTAssertTrue(storage.reader.readNextBatches(1).isEmpty, "There must be no other batches") + + storage.migrateUnauthorizedData(toConsent: .granted) + XCTAssertTrue( + storage.reader.readNextBatches(1).isEmpty, + "There must be no other batches, because pending events were deleted" + ) + } + + // MARK: - Data migration + + private let unauthorizedDirectory = temporaryFeatureDirectories.unauthorized + private let authorizedDirectory = temporaryFeatureDirectories.authorized + + func testDeletingPendingData() throws { + // Given + unauthorizedDirectory.createMockFiles(count: 10) + XCTAssertEqual(try unauthorizedDirectory.files().count, 10) + + // When + storage.clearUnauthorizedData() + + // Then + try queue.sync { + XCTAssertEqual(try unauthorizedDirectory.files().count, 0) + } + } + + func testDeletingAllData() throws { + // Given + unauthorizedDirectory.createMockFiles(count: 10) + authorizedDirectory.createMockFiles(count: 10) + XCTAssertEqual(try unauthorizedDirectory.files().count, 10) + XCTAssertEqual(try authorizedDirectory.files().count, 10) + + // When + storage.clearAllData() + + // Then + try queue.sync { + XCTAssertEqual(try unauthorizedDirectory.files().count, 0) + XCTAssertEqual(try authorizedDirectory.files().count, 0) + } + } +} diff --git a/DatadogCore/Tests/Datadog/Core/PerformancePresetTests.swift b/DatadogCore/Tests/Datadog/Core/PerformancePresetTests.swift new file mode 100644 index 0000000000..77988e44ce --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/PerformancePresetTests.swift @@ -0,0 +1,181 @@ +/* + * 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 +@testable import DatadogCore + +class PerformancePresetTests: XCTestCase { + func testIOSAppPresets() { + let smallBatchAnyFrequency = PerformancePreset(batchSize: .small, uploadFrequency: .mockRandom(), bundleType: .iOSApp) + XCTAssertEqual(smallBatchAnyFrequency.maxFileAgeForWrite, 2.85, accuracy: 0.01) + XCTAssertEqual(smallBatchAnyFrequency.minFileAgeForRead, 3.15, accuracy: 0.01) + XCTAssertEqual(smallBatchAnyFrequency.uploaderWindow, 3.0) + assertPresetCommonValues(in: smallBatchAnyFrequency) + + let mediumBatchAnyFrequency = PerformancePreset(batchSize: .medium, uploadFrequency: .mockRandom(), bundleType: .iOSApp) + XCTAssertEqual(mediumBatchAnyFrequency.maxFileAgeForWrite, 9.5) + XCTAssertEqual(mediumBatchAnyFrequency.minFileAgeForRead, 10.5) + XCTAssertEqual(mediumBatchAnyFrequency.uploaderWindow, 10.0) + assertPresetCommonValues(in: mediumBatchAnyFrequency) + + let largeBatchAnyFrequency = PerformancePreset(batchSize: .large, uploadFrequency: .mockRandom(), bundleType: .iOSApp) + XCTAssertEqual(largeBatchAnyFrequency.maxFileAgeForWrite, 33.25) + XCTAssertEqual(largeBatchAnyFrequency.minFileAgeForRead, 36.75) + XCTAssertEqual(largeBatchAnyFrequency.uploaderWindow, 35) + assertPresetCommonValues(in: largeBatchAnyFrequency) + + let frequentUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .frequent, bundleType: .iOSApp) + XCTAssertEqual(frequentUploadAnyBatch.initialUploadDelay, 2.5) + XCTAssertEqual(frequentUploadAnyBatch.minUploadDelay, 0.5) + XCTAssertEqual(frequentUploadAnyBatch.maxUploadDelay, 5.0) + XCTAssertEqual(frequentUploadAnyBatch.uploadDelayChangeRate, 0.1) + assertPresetCommonValues(in: frequentUploadAnyBatch) + + let averageUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .average, bundleType: .iOSApp) + XCTAssertEqual(averageUploadAnyBatch.initialUploadDelay, 10.0) + XCTAssertEqual(averageUploadAnyBatch.minUploadDelay, 2.0) + XCTAssertEqual(averageUploadAnyBatch.maxUploadDelay, 20.0) + XCTAssertEqual(averageUploadAnyBatch.uploadDelayChangeRate, 0.1) + assertPresetCommonValues(in: averageUploadAnyBatch) + + let rareUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .rare, bundleType: .iOSApp) + XCTAssertEqual(rareUploadAnyBatch.initialUploadDelay, 25.0) + XCTAssertEqual(rareUploadAnyBatch.minUploadDelay, 5.0) + XCTAssertEqual(rareUploadAnyBatch.maxUploadDelay, 50.0) + XCTAssertEqual(rareUploadAnyBatch.uploadDelayChangeRate, 0.1) + assertPresetCommonValues(in: rareUploadAnyBatch) + } + + func testIOSAppExtensionPresets() { + let smallBatchAnyFrequency = PerformancePreset(batchSize: .small, uploadFrequency: .mockRandom(), bundleType: .iOSAppExtension) + XCTAssertEqual(smallBatchAnyFrequency.maxFileAgeForWrite, 0.95) + XCTAssertEqual(smallBatchAnyFrequency.minFileAgeForRead, 1.05) + XCTAssertEqual(smallBatchAnyFrequency.uploaderWindow, 1) + assertPresetCommonValues(in: smallBatchAnyFrequency) + + let mediumBatchAnyFrequency = PerformancePreset(batchSize: .medium, uploadFrequency: .mockRandom(), bundleType: .iOSAppExtension) + XCTAssertEqual(mediumBatchAnyFrequency.maxFileAgeForWrite, 0.95, accuracy: 0.01) + XCTAssertEqual(mediumBatchAnyFrequency.minFileAgeForRead, 1.05, accuracy: 0.01) + XCTAssertEqual(mediumBatchAnyFrequency.uploaderWindow, 1) + assertPresetCommonValues(in: mediumBatchAnyFrequency) + + let largeBatchAnyFrequency = PerformancePreset(batchSize: .large, uploadFrequency: .mockRandom(), bundleType: .iOSAppExtension) + XCTAssertEqual(largeBatchAnyFrequency.maxFileAgeForWrite, 0.95, accuracy: 0.01) + XCTAssertEqual(largeBatchAnyFrequency.minFileAgeForRead, 1.05, accuracy: 0.01) + XCTAssertEqual(largeBatchAnyFrequency.uploaderWindow, 1) + assertPresetCommonValues(in: largeBatchAnyFrequency) + + let frequentUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .frequent, bundleType: .iOSAppExtension) + XCTAssertEqual(frequentUploadAnyBatch.initialUploadDelay, 0.25) + XCTAssertEqual(frequentUploadAnyBatch.minUploadDelay, 0.5) + XCTAssertEqual(frequentUploadAnyBatch.maxUploadDelay, 2.5) + XCTAssertEqual(frequentUploadAnyBatch.uploadDelayChangeRate, 0.5) + assertPresetCommonValues(in: frequentUploadAnyBatch) + + let averageUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .average, bundleType: .iOSAppExtension) + XCTAssertEqual(averageUploadAnyBatch.initialUploadDelay, 0.25) + XCTAssertEqual(averageUploadAnyBatch.minUploadDelay, 0.5) + XCTAssertEqual(averageUploadAnyBatch.maxUploadDelay, 2.5) + XCTAssertEqual(averageUploadAnyBatch.uploadDelayChangeRate, 0.5) + assertPresetCommonValues(in: averageUploadAnyBatch) + + let rareUploadAnyBatch = PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .rare, bundleType: .iOSAppExtension) + XCTAssertEqual(rareUploadAnyBatch.initialUploadDelay, 0.25) + XCTAssertEqual(rareUploadAnyBatch.minUploadDelay, 0.5) + XCTAssertEqual(rareUploadAnyBatch.maxUploadDelay, 2.5) + XCTAssertEqual(rareUploadAnyBatch.uploadDelayChangeRate, 0.5) + assertPresetCommonValues(in: rareUploadAnyBatch) + } + + private func assertPresetCommonValues(in preset: PerformancePreset) { + XCTAssertEqual(preset.maxFileSize, 4 * 1_024 * 1_024) // 4MB + XCTAssertEqual(preset.maxDirectorySize, 512 * 1_024 * 1_024) // 512 MB + XCTAssertEqual(preset.maxFileAgeForRead, 18 * 60 * 60) // 18h + XCTAssertEqual(preset.maxObjectsInFile, 500) + XCTAssertEqual(preset.maxObjectSize, 512 * 1_024) // 512KB + } + + func testPresetsConsistency() { + let allPossiblePresets: [PerformancePreset] = BatchSize.allCases + .combined(with: UploadFrequency.allCases) + .combined(with: BundleType.allCases) + .map { PerformancePreset(batchSize: $0.0, uploadFrequency: $0.1, bundleType: $1) } + + allPossiblePresets.forEach { preset in + XCTAssertLessThan( + preset.maxFileSize, + preset.maxDirectorySize, + "Size of individual file must not exceed the directory size limit." + ) + XCTAssertLessThan( + preset.maxFileAgeForWrite, + preset.minFileAgeForRead, + "File should not be considered for upload (read) while it is eligible for writes." + ) + XCTAssertGreaterThan( + preset.maxFileAgeForRead, + preset.minFileAgeForRead, + "File read boundaries must be consistent." + ) + XCTAssertGreaterThan( + preset.maxUploadDelay, + preset.minUploadDelay, + "Upload delay boundaries must be consistent." + ) + XCTAssertGreaterThan( + preset.maxUploadDelay, + preset.minUploadDelay, + "Upload delay boundaries must be consistent." + ) + XCTAssertLessThanOrEqual( + preset.uploadDelayChangeRate, + 1, + "Upload delay should not change by more than 100% at once." + ) + XCTAssertGreaterThan( + preset.uploadDelayChangeRate, + 0, + "Upload delay must change at non-zero rate." + ) + } + } + + func testPresetsUpdate() { + // Given + let maxFileSizeOverride: UInt64 = .mockRandom() + let maxObjectSizeOverride: UInt64 = .mockRandom() + let meanFileAgeOverride: TimeInterval = .mockRandom(min: 1, max: 100) + let uploadDelayOverride: (initial: TimeInterval, range: Range, changeRate: Double) = ( + initial: .mockRandom(), + range: (TimeInterval.mockRandom(min: 1, max: 10).. String { + return UUID().uuidString + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift new file mode 100644 index 0000000000..c97b1d10b5 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Files/FileTests.swift @@ -0,0 +1,113 @@ +/* + * 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 DatadogCore + +class FileTests: XCTestCase { + private let fileManager = FileManager.default + lazy var directory = Directory(url: temporaryDirectory) + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + func testItAppendsDataToFile() throws { + let file = try directory.createFile(named: "file") + + try file.append(data: Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes + + XCTAssertEqual( + try Data(contentsOf: file.url), + Data([0x41, 0x41, 0x41, 0x41, 0x41]) + ) + + try file.append(data: Data([0x42, 0x42, 0x42, 0x42, 0x42])) // 5 bytes + try file.append(data: Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes + + XCTAssertEqual( + try Data(contentsOf: file.url), + Data( + [ + 0x41, 0x41, 0x41, 0x41, 0x41, + 0x42, 0x42, 0x42, 0x42, 0x42, + 0x41, 0x41, 0x41, 0x41, 0x41, + ] + ) + ) + } + + func testItReadsDataFromFile() throws { + let file = try directory.createFile(named: "file") + let data = "Hello 👋".utf8Data + try file.append(data: data) + + let stream = try file.stream() + stream.open() + defer { stream.close() } + + var bytes = [UInt8](repeating: 0, count: data.count) + XCTAssertEqual(stream.read(&bytes, maxLength: data.count), data.count) + XCTAssertEqual(String(bytes: bytes, encoding: .utf8), "Hello 👋") + } + + func testItDeletesFile() throws { + let file = try directory.createFile(named: "file") + XCTAssertTrue(fileManager.fileExists(atPath: file.url.path)) + + try file.delete() + XCTAssertFalse(fileManager.fileExists(atPath: file.url.path)) + } + + func testItReturnsFileSize() throws { + let file = try directory.createFile(named: "file") + + try file.append(data: .mock(ofSize: 5)) + XCTAssertEqual(try file.size(), 5) + + try file.append(data: .mock(ofSize: 10)) + XCTAssertEqual(try file.size(), 15) + } + + func testWhenIOExceptionHappens_itThrowsWhenWriting() throws { + let file = try directory.createFile(named: "file") + try file.delete() + + XCTAssertThrowsError(try file.append(data: .mock(ofSize: 5))) { error in + 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 new file mode 100644 index 0000000000..d6a8d88b46 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift @@ -0,0 +1,182 @@ +/* + * 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 +@testable import DatadogCore + +class FilesOrchestrator_MetricsTests: XCTestCase { + private let telemetry = TelemetryMock() + private let dateProvider = RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + private var storage: StoragePerformanceMock! // swiftlint:disable:this implicitly_unwrapped_optional + private var upload: UploadPerformanceMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + + let performance: PerformancePreset = .mockRandom() + storage = StoragePerformanceMock(other: performance) + upload = UploadPerformanceMock(other: performance) + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + private func createOrchestrator() -> FilesOrchestrator { + return FilesOrchestrator( + directory: Directory(url: temporaryDirectory), + performance: PerformancePreset.combining(storagePerformance: storage, uploadPerformance: upload), + dateProvider: dateProvider, + telemetry: telemetry, + metricsData: FilesOrchestrator.MetricsData( + trackName: "track name", + consentLabel: "consent value", + uploaderPerformance: upload, + backgroundTasksEnabled: .mockAny() + ) + ) + } + + // MARK: - "Batch Deleted" Metric + + func testWhenReadableFileIsDeleted_itSendsBatchDeletedMetric() throws { + // Given + let orchestrator = createOrchestrator() + let file = try XCTUnwrap(orchestrator.getWritableFile(writeSize: 1) as? ReadableFile) + let expectedBatchAge = storage.minFileAgeForRead + 1 + + // When: + // - wait and delete the file + dateProvider.advance(bySeconds: expectedBatchAge) + orchestrator.delete(readableFile: file, deletionReason: .intakeCode(responseCode: 202)) + + // Then + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + DDAssertReflectionEqual(metric.attributes, [ + "metric_type": "batch deleted", + "track": "track name", + "consent": "consent value", + "uploader_delay": [ + "min": upload.minUploadDelay.toMilliseconds, + "max": upload.maxUploadDelay.toMilliseconds + ], + "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 { + // Given: + // - request some batch to be created + let orchestrator = createOrchestrator() + _ = try orchestrator.getWritableFile(writeSize: 1) + + // When: + // - wait more than batch obsolescence limit + // - then request readable file, which should trigger obsolete files deletion + dateProvider.advance(bySeconds: storage.maxFileAgeForRead + 1) + _ = orchestrator.getReadableFiles() + + // Then + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + DDAssertReflectionEqual(metric.attributes, [ + "metric_type": "batch deleted", + "track": "track name", + "consent": "consent value", + "uploader_delay": [ + "min": upload.minUploadDelay.toMilliseconds, + "max": upload.maxUploadDelay.toMilliseconds + ], + "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 { + // Given: some batch + // - request batch to be created + // - write more data than allowed directory size limit + storage.maxDirectorySize = 10 // 10 bytes + let orchestrator = createOrchestrator() + let file = try orchestrator.getWritableFile(writeSize: storage.maxDirectorySize + 1) + try file.append(data: .mockRandom(ofSize: storage.maxDirectorySize + 1)) + let expectedBatchAge = storage.minFileAgeForRead + 1 + + // When: + // - then request new batch, which triggers directory purging + dateProvider.advance(bySeconds: expectedBatchAge) + _ = try orchestrator.getWritableFile(writeSize: 1) + + // Then + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Deleted")) + DDAssertReflectionEqual(metric.attributes, [ + "metric_type": "batch deleted", + "track": "track name", + "consent": "consent value", + "uploader_delay": [ + "min": upload.minUploadDelay.toMilliseconds, + "max": upload.maxUploadDelay.toMilliseconds + ], + "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 + + func testWhenNewBatchIsStarted_itSendsBatchClosedMetric() throws { + // Given + // - request batch to be created + // - request few writes on that batch, each after certain delay + let orchestrator = createOrchestrator() + let expectedWrites: [UInt64] = [10, 5, 2] + let expectedWriteDelays: [TimeInterval] = [ + storage.maxFileAgeForWrite * 0.25, + storage.maxFileAgeForWrite * 0.45, + ] + + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[0]) + dateProvider.advance(bySeconds: expectedWriteDelays[0]) + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[1]) + dateProvider.advance(bySeconds: expectedWriteDelays[1]) + _ = try orchestrator.getWritableFile(writeSize: expectedWrites[2]) + + // When + // - wait more than allowed batch age for writes, so next batch request will create another batch + // - then request another batch, which will close the previous one + dateProvider.advance(bySeconds: storage.maxFileAgeForWrite + 1) + _ = try orchestrator.getWritableFile(writeSize: 1) + + // Then + let metric = try XCTUnwrap(telemetry.messages.firstMetric(named: "Batch Closed")) + DDAssertReflectionEqual(metric.attributes, [ + "metric_type": "batch closed", + "track": "track name", + "consent": "consent value", + "uploader_window": storage.uploaderWindow.toMilliseconds, + "batch_size": expectedWrites.reduce(0, +), + "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 new file mode 100644 index 0000000000..cb781d6f97 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -0,0 +1,332 @@ +/* + * 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 +@testable import DatadogCore + +class FilesOrchestratorTests: XCTestCase { + private let performance: PerformancePreset = .mockRandom() + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + /// Configures `FilesOrchestrator` under tests. + private func configureOrchestrator(using dateProvider: DateProvider) -> FilesOrchestrator { + return FilesOrchestrator( + directory: .init(url: temporaryDirectory), + performance: performance, + dateProvider: dateProvider, + telemetry: NOPTelemetry() + ) + } + + // MARK: - Writable file tests + + func testWhenWritableFileIsObtainedFirstTime_itCreatesNewFile() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + + _ = try orchestrator.getWritableFile(writeSize: 1) + + XCTAssertEqual(try orchestrator.directory.files().count, 1) + XCTAssertNotNil(try orchestrator.directory.file(named: dateProvider.now.toFileName)) + } + + func testWhenWritableFileIsObtainedAnotherTime_itReusesSameFile() throws { + let orchestrator = configureOrchestrator(using: RelativeDateProvider(advancingBySeconds: 0.001)) + let file1 = try orchestrator.getWritableFile(writeSize: 1) + + let file2 = try orchestrator.getWritableFile(writeSize: 1) + + XCTAssertEqual(try orchestrator.directory.files().count, 1) + XCTAssertEqual(file1.name, file2.name) + } + + func testWhenSameWritableFileWasUsedMaxNumberOfTimes_itCreatesNewFile() throws { + let dateProvider = DateProviderMock() + let orchestrator = configureOrchestrator(using: dateProvider) + var previousFile: WritableFile = try orchestrator.getWritableFile(writeSize: 1) // first use of a new file + var nextFile: WritableFile + + for _ in (0..<5) { + for _ in (0 ..< performance.maxObjectsInFile).dropLast() { // skip first use + dateProvider.now.addTimeInterval(0.001) + nextFile = try orchestrator.getWritableFile(writeSize: 1) + XCTAssertEqual(nextFile.name, previousFile.name, "It should reuse the file \(performance.maxObjectsInFile) times") + previousFile = nextFile + } + + dateProvider.now.addTimeInterval(0.001) + nextFile = try orchestrator.getWritableFile(writeSize: 1) // first use of a new file + XCTAssertNotEqual(nextFile.name, previousFile.name, "It should create a new file when previous one is used \(performance.maxObjectsInFile) times") + previousFile = nextFile + } + } + + func testWhenWritableFileHasNoEnoughSpaceLeft_itCreatesNewFile() throws { + let orchestrator = configureOrchestrator(using: RelativeDateProvider(advancingBySeconds: 0.001)) + let chunkedData: [Data] = .mockChunksOf( + totalSize: performance.maxFileSize, + maxChunkSize: performance.maxObjectSize + ) + + let file1 = try orchestrator.getWritableFile(writeSize: performance.maxObjectSize) + try chunkedData.forEach { chunk in try file1.append(data: chunk) } + + let file2 = try orchestrator.getWritableFile(writeSize: 1) + XCTAssertNotEqual(file1.name, file2.name) + } + + func testWhenWritableFileIsTooOld_itCreatesNewFile() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + let file1 = try orchestrator.getWritableFile(writeSize: 1) + + dateProvider.advance(bySeconds: 1 + performance.maxFileAgeForWrite) + + let file2 = try orchestrator.getWritableFile(writeSize: 1) + XCTAssertNotEqual(file1.name, file2.name) + } + + func testWhenWritableFileWasDeleted_itCreatesNewFile() throws { + let orchestrator = configureOrchestrator(using: RelativeDateProvider(advancingBySeconds: 0.001)) + let file1 = try orchestrator.getWritableFile(writeSize: 1) + + try orchestrator.directory.files().forEach { try $0.delete() } + + let file2 = try orchestrator.getWritableFile(writeSize: 1) + XCTAssertNotEqual(file1.name, file2.name) + } + + /// This test makes sure that if SDK is used by multiple processes simultaneously, each `FileOrchestrator` works on a separate writable file. + /// It is important when SDK is used by iOS App and iOS App Extension at the same time. + func testWhenRequestedFirstTime_eachOrchestratorInstanceCreatesNewWritableFile() throws { + let orchestrator1 = configureOrchestrator(using: RelativeDateProvider()) + let orchestrator2 = configureOrchestrator( + using: RelativeDateProvider(startingFrom: Date().secondsAgo(0.01)) // simulate time difference + ) + + _ = try orchestrator1.getWritableFile(writeSize: 1) + XCTAssertEqual(try orchestrator1.directory.files().count, 1) + + _ = try orchestrator2.getWritableFile(writeSize: 1) + XCTAssertEqual(try orchestrator2.directory.files().count, 2) + } + + func testWhenFilesDirectorySizeIsBig_itKeepsItUnderLimit_byRemovingOldestFilesFirst() throws { + let oneMB = 1.MB.asUInt64() + + let orchestrator = FilesOrchestrator( + directory: .init(url: temporaryDirectory), + performance: StoragePerformanceMock( + maxFileSize: oneMB, // 1MB + maxDirectorySize: 3 * oneMB, // 3MB, + maxFileAgeForWrite: .distantFuture, + minFileAgeForRead: .mockAny(), + maxFileAgeForRead: .mockAny(), + maxObjectsInFile: 1, // create new file each time + maxObjectSize: .max + ), + dateProvider: RelativeDateProvider(advancingBySeconds: 1), + telemetry: NOPTelemetry() + ) + + // write 1MB to first file (1MB of directory size in total) + let file1 = try orchestrator.getWritableFile(writeSize: oneMB) + try file1.append(data: .mock(ofSize: oneMB)) + + // write 1MB to second file (2MB of directory size in total) + let file2 = try orchestrator.getWritableFile(writeSize: oneMB) + try file2.append(data: .mock(ofSize: oneMB)) + + // write 1MB to third file (3MB of directory size in total) + let file3 = try orchestrator.getWritableFile(writeSize: oneMB + 1) // +1 byte to exceed the limit + try file3.append(data: .mock(ofSize: oneMB + 1)) + + XCTAssertEqual(try orchestrator.directory.files().count, 3) + + // At this point, directory reached its maximum size. + // Asking for the next file should purge the oldest one. + let file4 = try orchestrator.getWritableFile(writeSize: oneMB) + XCTAssertEqual(try orchestrator.directory.files().count, 3) + XCTAssertNil(try? orchestrator.directory.file(named: file1.name)) + try file4.append(data: .mock(ofSize: oneMB + 1)) + + _ = try orchestrator.getWritableFile(writeSize: oneMB) + XCTAssertEqual(try orchestrator.directory.files().count, 3) + 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() { + let dateProvider = RelativeDateProvider() + + let orchestrator = configureOrchestrator(using: dateProvider) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + + XCTAssertTrue(orchestrator.getReadableFiles().isEmpty) + } + + func testWhenReadableFileIsOldEnough_itReturnsFiles() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + _ = try orchestrator.directory.createFile(named: dateProvider.now.toFileName) + + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + + XCTAssertGreaterThan(orchestrator.getReadableFiles().count, 0) + } + + func testWhenReadableFilesAreNotOldEnough_itReturnsEmpty() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + _ = try orchestrator.directory.createFile(named: dateProvider.now.toFileName) + + dateProvider.advance(bySeconds: 0.5 * performance.minFileAgeForRead) + + XCTAssertTrue(orchestrator.getReadableFiles().isEmpty) + } + + func testWhenThereAreMultipleReadableFiles_itReturnsSortedFromOldestFile() throws { + let dateProvider = RelativeDateProvider(advancingBySeconds: 1) + let orchestrator = configureOrchestrator(using: dateProvider) + + let fileNames = (0..<4).map { _ in dateProvider.now.toFileName } + try fileNames.forEach { fileName in _ = try orchestrator.directory.createFile(named: fileName) } + + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + let readableFiles = orchestrator.getReadableFiles() + XCTAssertEqual(readableFiles[0].name, fileNames[0]) + XCTAssertEqual(readableFiles[1].name, fileNames[1]) + XCTAssertEqual(readableFiles[2].name, fileNames[2]) + XCTAssertEqual(readableFiles[3].name, fileNames[3]) + } + + func testsWhenThereAreMultipleReadableFiles_itReturnsFilesByExcludingCertainNames() throws { + let dateProvider = RelativeDateProvider(advancingBySeconds: 1) + let orchestrator = configureOrchestrator(using: dateProvider) + + let fileNames = (0..<4).map { _ in dateProvider.now.toFileName } + try fileNames.forEach { fileName in _ = try orchestrator.directory.createFile(named: fileName) } + + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + let readableFiles = orchestrator.getReadableFiles(excludingFilesNamed: Set(fileNames[0...2])) + XCTAssertEqual(readableFiles.count, 1) + XCTAssertEqual(readableFiles.first?.name, fileNames.last) + } + + func testWhenReadableFilesAreTooOld_theyGetDeleted() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + _ = try orchestrator.directory.createFile(named: dateProvider.now.toFileName) + + dateProvider.advance(bySeconds: 2 * performance.maxFileAgeForRead) + + XCTAssertTrue(orchestrator.getReadableFiles().isEmpty) + XCTAssertEqual(try orchestrator.directory.files().count, 0) + } + + func testWhenThereAreMultipleReadableFiles_itRespectsTheLimit() throws { + let dateProvider = RelativeDateProvider(advancingBySeconds: 1) + let orchestrator = configureOrchestrator(using: dateProvider) + + let fileNames = (0..<4).map { _ in dateProvider.now.toFileName } + try fileNames.forEach { fileName in _ = try orchestrator.directory.createFile(named: fileName) } + + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + let limit = 2 + let readableFiles = orchestrator.getReadableFiles(limit: limit) + + XCTAssertEqual(readableFiles.count, limit) + XCTAssertEqual(readableFiles[0].name, fileNames[0]) + XCTAssertEqual(readableFiles[1].name, fileNames[1]) + } + + // MARK: - Deleting Files + + func testItDeletesReadableFiles() throws { + let dateProvider = RelativeDateProvider() + let orchestrator = configureOrchestrator(using: dateProvider) + _ = try orchestrator.directory.createFile(named: dateProvider.now.toFileName) + + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) + + let readableFile = try orchestrator.getReadableFiles().first.unwrapOrThrow() + XCTAssertEqual(try orchestrator.directory.files().count, 1) + orchestrator.delete(readableFile: readableFile) + XCTAssertEqual(try orchestrator.directory.files().count, 0) + } + + // MARK: - File names tests + + // swiftlint:disable number_separator + func testItTurnsFileNameIntoFileCreationDate() { + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 0)), "0") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456)), "123456000") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.7)), "123456700") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.78)), "123456780") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.789)), "123456789") + + // microseconds rounding + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.1111)), "123456111") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.1115)), "123456112") + XCTAssertEqual(fileNameFrom(fileCreationDate: Date(timeIntervalSinceReferenceDate: 123456.1119)), "123456112") + + // overflows + let maxDate = Date(timeIntervalSinceReferenceDate: TimeInterval.greatestFiniteMagnitude) + let minDate = Date(timeIntervalSinceReferenceDate: -TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(fileNameFrom(fileCreationDate: maxDate), "0") + XCTAssertEqual(fileNameFrom(fileCreationDate: minDate), "0") + } + + func testItTurnsFileCreationDateIntoFileName() { + XCTAssertEqual(fileCreationDateFrom(fileName: "0"), Date(timeIntervalSinceReferenceDate: 0)) + XCTAssertEqual(fileCreationDateFrom(fileName: "123456000"), Date(timeIntervalSinceReferenceDate: 123456)) + XCTAssertEqual(fileCreationDateFrom(fileName: "123456700"), Date(timeIntervalSinceReferenceDate: 123456.7)) + XCTAssertEqual(fileCreationDateFrom(fileName: "123456780"), Date(timeIntervalSinceReferenceDate: 123456.78)) + XCTAssertEqual(fileCreationDateFrom(fileName: "123456789"), Date(timeIntervalSinceReferenceDate: 123456.789)) + + // ignores invalid names + let invalidFileName = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + XCTAssertEqual(fileCreationDateFrom(fileName: invalidFileName), Date(timeIntervalSinceReferenceDate: 0)) + } + // swiftlint:enable number_separator +} diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Reading/FileReaderTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Reading/FileReaderTests.swift new file mode 100644 index 0000000000..fc2240287f --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Reading/FileReaderTests.swift @@ -0,0 +1,165 @@ +/* + * 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 +@testable import DatadogCore + +class FileReaderTests: XCTestCase { + lazy var directory = Directory(url: temporaryDirectory) + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + func testItReadsBatches() throws { + let reader = FileReader( + orchestrator: FilesOrchestrator( + directory: directory, + performance: StoragePerformanceMock.readAllFiles, + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + let dataProvider = RelativeDateProvider() + let dataBlocks = [ + BatchDataBlock(type: .eventMetadata, data: "EFGH".utf8Data), + BatchDataBlock(type: .event, data: "ABCD".utf8Data) + ] + let data = try dataBlocks + .map { try $0.serialize() } + .reduce(.init(), +) + _ = try directory + .createFile(named: dataProvider.now.toFileName) + .append(data: data) + + XCTAssertEqual(try directory.files().count, 1) + XCTAssertEqual(reader.readNextBatches(.max).count, 1) + let batch = reader.readNextBatches(1).first + + let expected = [ + Event(data: "ABCD".utf8Data, metadata: "EFGH".utf8Data) + ] + XCTAssertEqual(batch?.events, expected) + + dataProvider.advance(bySeconds: .mockRandom()) + _ = try directory + .createFile(named: dataProvider.now.toFileName) + .append(data: data) + + XCTAssertEqual(try directory.files().count, 2) + XCTAssertEqual(reader.readNextBatches(2).count, 2) + XCTAssertEqual(reader.readNextBatches(.max).count, 2) + } + + func testItReadsEncryptedBatches() throws { + let dataBlocks = [ + BatchDataBlock(type: .eventMetadata, data: "foo".utf8Data), + BatchDataBlock(type: .event, data: "foo".utf8Data), + BatchDataBlock(type: .event, data: "foo".utf8Data), + BatchDataBlock(type: .eventMetadata, data: "foo".utf8Data), + BatchDataBlock(type: .event, data: "foo".utf8Data) + ] + let data = try dataBlocks + .map { Data(try $0.serialize()) } + .reduce(.init(), +) + + let dataProvider = RelativeDateProvider() + + _ = try directory + .createFile(named: dataProvider.now.toFileName) + .append(data: data) + + let reader = FileReader( + orchestrator: FilesOrchestrator( + directory: directory, + performance: StoragePerformanceMock.readAllFiles, + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: DataEncryptionMock( + decrypt: { _ in "bar".utf8Data } + ), + telemetry: NOPTelemetry() + ) + + XCTAssertEqual(reader.readNextBatches(.max).count, 1) + let batch = reader.readNextBatches(1).first + + let expected = [ + Event(data: "bar".utf8Data, metadata: "bar".utf8Data), + Event(data: "bar".utf8Data, metadata: nil), + Event(data: "bar".utf8Data, metadata: "bar".utf8Data) + ] + XCTAssertEqual(batch?.events, expected) + + dataProvider.advance(bySeconds: .mockRandom()) + _ = try directory + .createFile(named: dataProvider.now.toFileName) + .append(data: data) + + XCTAssertEqual(reader.readNextBatches(2).count, 2) + XCTAssertEqual(reader.readNextBatches(.max).count, 2) + } + + func testItMarksBatchesAsRead() throws { + let dateProvider = RelativeDateProvider(advancingBySeconds: 60) + let reader = FileReader( + orchestrator: FilesOrchestrator( + directory: directory, + performance: StoragePerformanceMock.readAllFiles, + dateProvider: dateProvider, + telemetry: NOPTelemetry() + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + let file1 = try directory.createFile(named: dateProvider.now.toFileName) + try file1.append(data: BatchDataBlock(type: .eventMetadata, data: "2".utf8Data).serialize()) + try file1.append(data: BatchDataBlock(type: .event, data: "1".utf8Data).serialize()) + + let file2 = try directory.createFile(named: dateProvider.now.toFileName) + try file2.append(data: BatchDataBlock(type: .event, data: "2".utf8Data).serialize()) + + let file3 = try directory.createFile(named: dateProvider.now.toFileName) + try file3.append(data: BatchDataBlock(type: .eventMetadata, data: "4".utf8Data).serialize()) + try file3.append(data: BatchDataBlock(type: .event, data: "3".utf8Data).serialize()) + + let expected = [ + Event(data: "1".utf8Data, metadata: "2".utf8Data), + Event(data: "2".utf8Data, metadata: nil), + Event(data: "3".utf8Data, metadata: "4".utf8Data) + ] + + let batch: Batch + batch = try reader.readNextBatches(1).first.unwrapOrThrow() + XCTAssertEqual(batch.events.first, expected[0]) + reader.markBatchAsRead(batch) + + let batches = reader.readNextBatches(2) + XCTAssertEqual(batches[0].events.first, expected[1]) + XCTAssertEqual(batches[1].events.first, expected[2]) + batches.forEach { reader.markBatchAsRead($0) } + + XCTAssertTrue(reader.readNextBatches(1).isEmpty) + XCTAssertEqual(try directory.files().count, 0) + } +} + +extension Reader { + func readNextBatches(_ limit: Int = .max) -> [Batch] { + return readFiles(limit: limit).compactMap { readBatch(from: $0) } + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Storage+TLVTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Storage+TLVTests.swift new file mode 100644 index 0000000000..48e8417478 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Storage+TLVTests.swift @@ -0,0 +1,41 @@ +/* + * 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 DatadogCore + +class Storage_TLVTests: XCTestCase { + func testSerializeEventBlock() throws { + // Given + let eventData: Data = .mockRandom(ofSize: 100) + + // When + let tlvData = try BatchDataBlock(type: .event, data: eventData).serialize() + + // Then + let expectedT = Data([0x00, 0x00]) + let expectedL = Data([0x64, 0x00, 0x00, 0x00]) // 100 in hex + let expectedV = eventData + + XCTAssertEqual(tlvData, expectedT + expectedL + expectedV) + } + + func testSerializeEventMetadataBlock() throws { + // Given + let eventMetadata: Data = .mockRandom(ofSize: 100) + + // When + let tlvData = try BatchDataBlock(type: .eventMetadata, data: eventMetadata).serialize() + + // Then + let expectedT = Data([0x01, 0x00]) + let expectedL = Data([0x64, 0x00, 0x00, 0x00]) // 100 in hex + let expectedV = eventMetadata + + XCTAssertEqual(tlvData, expectedT + expectedL + expectedV) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift new file mode 100644 index 0000000000..6c6dacf7a5 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Persistence/Writing/FileWriterTests.swift @@ -0,0 +1,354 @@ +/* + * 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 + +@testable import DatadogCore + +class FileWriterTests: XCTestCase { + lazy var directory = Directory(url: temporaryDirectory) + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + func testItWritesDataWithMetadataToSingleFileInTLVFormat() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) + writer.write(value: ["key2": "value2"]) // skipped metadata here + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) + + XCTAssertEqual(try directory.files().count, 1) + let stream = try directory.files()[0].stream() + + let reader = BatchDataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"{"meta1":"metaValue1"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key1":"value1"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key2":"value2"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"{"meta3":"metaValue3"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key3":"value3"}"#.utf8Data) + } + + func testItWritesEncryptedDataWithMetadataToSingleFileInTLVFormat() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: DataEncryptionMock( + encrypt: { data in + "encrypted".utf8Data + data + "encrypted".utf8Data + } + ), + telemetry: NOPTelemetry() + ) + + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) + writer.write(value: ["key2": "value2"]) // skipped metadata here + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) + + XCTAssertEqual(try directory.files().count, 1) + let stream = try directory.files()[0].stream() + + let reader = BatchDataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"encrypted{"meta1":"metaValue1"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key1":"value1"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key2":"value2"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, #"encrypted{"meta3":"metaValue3"}encrypted"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"encrypted{"key3":"value3"}encrypted"#.utf8Data) + } + + func testItWritesDataToSingleFileInTLVFormat() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + writer.write(value: ["key1": "value1"]) + writer.write(value: ["key2": "value2"]) + writer.write(value: ["key3": "value3"]) + + XCTAssertEqual(try directory.files().count, 1) + let stream = try directory.files()[0].stream() + + let reader = BatchDataBlockReader(input: stream) + var block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key1":"value1"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key2":"value2"}"#.utf8Data) + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, #"{"key3":"value3"}"#.utf8Data) + } + + func testGivenErrorVerbosity_whenIndividualDataExceedsMaxWriteSize_itDropsDataAndPrintsError() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, + minFileAgeForRead: .mockAny(), + maxFileAgeForRead: .mockAny(), + maxObjectsInFile: .max, + maxObjectSize: 23 // 23 bytes is enough for TLV with {"key1":"value1"} JSON + ), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry(), + metricsData: .init( + trackName: "rum", + consentLabel: .mockAny(), + uploaderPerformance: UploadPerformanceMock.noOp, + backgroundTasksEnabled: .mockAny() + ) + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + writer.write(value: ["key1": "value1"]) // will be written + + XCTAssertEqual(try directory.files().count, 1) + var reader = try BatchDataBlockReader(input: directory.files()[0].stream()) + var blocks = try XCTUnwrap(reader.all()) + XCTAssertEqual(blocks.count, 1) + XCTAssertEqual(blocks[0].data, #"{"key1":"value1"}"#.utf8Data) + + writer.write(value: ["key2": "value3 that makes it exceed 23 bytes"]) // will be dropped + + 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, "(rum) Failed to encode value") + XCTAssertEqual(dd.logger.errorLog?.error?.message, "DataBlock length exceeds limit of 23 bytes") + } + + func testGivenErrorVerbosity_whenDataCannotBeEncoded_itPrintsError() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry(), + metricsData: .init( + trackName: "rum", + consentLabel: .mockAny(), + uploaderPerformance: UploadPerformanceMock.noOp, + backgroundTasksEnabled: .mockAny() + ) + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + writer.write(value: FailingEncodableMock(errorMessage: "failed to encode `FailingEncodable`.")) + + XCTAssertEqual(dd.logger.errorLog?.message, "(rum) Failed to encode value") + XCTAssertEqual(dd.logger.errorLog?.error?.message, "failed to encode `FailingEncodable`.") + } + + func testGivenErrorVerbosity_whenIOExceptionIsThrown_itPrintsError() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry(), + metricsData: .init( + trackName: "rum", + consentLabel: .mockAny(), + uploaderPerformance: UploadPerformanceMock.noOp, + backgroundTasksEnabled: .mockAny() + ) + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + writer.write(value: ["ok"]) // will create the file + try? directory.files()[0].makeReadonly() + writer.write(value: ["won't be written"]) + try? directory.files()[0].makeReadWrite() + + 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")) + } + + /// NOTE: Test added after incident-4797 + func testWhenIOExceptionsHappenRandomly_theFileIsNeverMalformed() throws { + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write to single file + minFileAgeForRead: .distantFuture, + maxFileAgeForRead: .distantFuture, + maxObjectsInFile: .max, // write to single file + maxObjectSize: .max + ), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: nil, + telemetry: NOPTelemetry() + ) + + let ioInterruptionQueue = DispatchQueue(label: "com.datadohq.file-writer-random-io") + + func randomlyInterruptIO(for file: File?) { + ioInterruptionQueue.async { try? file?.makeReadonly() } + ioInterruptionQueue.async { try? file?.makeReadWrite() } + } + + struct Foo: Codable, Equatable { + var foo = "bar" + } + + struct Metadata: Codable, Equatable { + var meta = "data" + } + + let foo = Foo() + let metadata = Metadata() + + // Write 300 of `Foo`s and interrupt writes randomly + (0..<300).forEach { _ in + writer.write(value: foo, metadata: metadata) + randomlyInterruptIO(for: try? directory.files().first) + } + + ioInterruptionQueue.sync { } + + XCTAssertEqual(try directory.files().count, 1) + + let stream = try directory.files()[0].stream() + let blocks = try BatchDataBlockReader(input: stream).all() + + // Assert that data written is not malformed + let jsonDecoder = JSONDecoder() + let eventGenerator = EventGenerator(dataBlocks: blocks) + let events = eventGenerator.map { $0 } + + // Assert that some (including all) `Foo`s were written + XCTAssertGreaterThan(events.count, 0) + XCTAssertLessThanOrEqual(events.count, 300) + for event in events { + let actualFoo = try jsonDecoder.decode(Foo.self, from: event.data) + XCTAssertEqual(actualFoo, foo) + + XCTAssertNotNil(event.metadata) + let actualMetadata = try jsonDecoder.decode(Metadata.self, from: event.metadata!) + XCTAssertEqual(actualMetadata, metadata) + } + } + + func testItWritesEncryptedDataToSingleFile() throws { + // Given + let writer = FileWriter( + orchestrator: FilesOrchestrator( + directory: directory, + performance: PerformancePreset.mockAny(), + dateProvider: SystemDateProvider(), + telemetry: NOPTelemetry() + ), + encryption: DataEncryptionMock( + encrypt: { _ in "foo".utf8Data } + ), + telemetry: NOPTelemetry() + ) + + // When + writer.write(value: ["key1": "value1"], metadata: ["meta1": "metaValue1"]) + writer.write(value: ["key2": "value3"]) + writer.write(value: ["key3": "value3"], metadata: ["meta3": "metaValue3"]) + + // Then + XCTAssertEqual(try directory.files().count, 1) + let stream = try directory.files()[0].stream() + + let reader = BatchDataBlockReader(input: stream) + + var block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() + XCTAssertEqual(block?.type, .eventMetadata) + XCTAssertEqual(block?.data, "foo".utf8Data) + + block = try reader.next() + XCTAssertEqual(block?.type, .event) + XCTAssertEqual(block?.data, "foo".utf8Data) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockReaderTests.swift b/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockReaderTests.swift new file mode 100644 index 0000000000..cb7095c3ab --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockReaderTests.swift @@ -0,0 +1,159 @@ +/* + * 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 DatadogCore + +private enum BlockType: UInt16, CaseIterable { + case one = 0x01 + case two = 0x02 + case three = 0x03 +} + +private typealias Block = TLVBlock +private typealias BlockReader = TLVBlockReader + +class TLVBlockReaderTests: XCTestCase { + func testReadingNextBlocks() throws { + let block1 = Data([0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xAA]) + let block2 = Data([0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0xAA, 0xBB]) + let block3 = Data([0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC]) + // ^ type ^ ^ data size ^ ^ data ^ + + let reader = BlockReader(data: block1 + block2 + block3) + var currentBlock: Block + + currentBlock = try XCTUnwrap(reader.next()) + XCTAssertEqual(currentBlock.type, .one) + XCTAssertEqual(currentBlock.data, Data([0xAA])) + + currentBlock = try XCTUnwrap(reader.next()) + XCTAssertEqual(currentBlock.type, .two) + XCTAssertEqual(currentBlock.data, Data([0xAA, 0xBB])) + + currentBlock = try XCTUnwrap(reader.next()) + XCTAssertEqual(currentBlock.type, .three) + XCTAssertEqual(currentBlock.data, Data([0xAA, 0xBB, 0xCC])) + + XCTAssertNil(try reader.next()) + } + + func testReadingNextBlocks_whenEmpty() throws { + let reader = BlockReader(data: Data()) + XCTAssertNil(try reader.next()) + } + + func testWhenReadingNextBlocks_itSkipsUnknownTypes() throws { + let data = Data( + [ + 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, // <- unknown type: 0x00, 0xFF + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, // <- known type: 0x01, 0x00 + 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, // <- unknown type: 0x00, 0xFF + ] + ) + let reader = BlockReader(data: data) + let block = try XCTUnwrap(reader.next()) + XCTAssertEqual(block.type, .one) + XCTAssertEqual(block.data, Data([0xFF])) + XCTAssertNil(try reader.next()) + } + + func testReadingAllBlocks() throws { + let data = try (0..<100).map { idx in + try Block( + type: BlockType.allCases[idx % BlockType.allCases.count], + data: .mock(ofSize: idx) + ).serialize() + } + .reduce(Data(), +) + + let reader = BlockReader(data: data) + let blocks = try reader.all() + + XCTAssertEqual(blocks.count, 100) + XCTAssertEqual(blocks.filter({ $0.type == .one }).count, 34) + XCTAssertEqual(blocks.filter({ $0.type == .two }).count, 33) + XCTAssertEqual(blocks.filter({ $0.type == .three }).count, 33) + XCTAssertEqual(blocks.first?.data.count, 0) + XCTAssertEqual(blocks.last?.data.count, 99) + } + + func testReadingZeroBytesBlock() throws { + let data = Data( + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, + 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF + ] + ) + let reader = BlockReader(data: data) + + var block = try reader.next() + XCTAssertEqual(block?.type, .one) + XCTAssertEqual(block?.data, Data()) + + block = try reader.next() + XCTAssertEqual(block?.type, .two) + XCTAssertEqual(block?.data, Data([0xFF])) + + block = try reader.next() + XCTAssertEqual(block?.type, .three) + XCTAssertEqual(block?.data, Data([0xFF, 0xFF])) + } + + func testReadingBytesUnderLengthLimit() throws { + let data = Data([0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF]) + let reader = BlockReader(data: data, maxBlockLength: 2) + + let block = try XCTUnwrap(reader.next()) + XCTAssertEqual(block.type, .one) + XCTAssertEqual(block.data.first, 0xFF) + XCTAssertEqual(block.data.count, 2) + } + + func testSkippingExceedingBytesLengthLimit() throws { + let data = Data([0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF]) + let reader = BlockReader(data: data, maxBlockLength: 1) + + XCTAssertThrowsError(try reader.next()) { error in + guard let error = error as? TLVBlockError, case TLVBlockError.bytesLengthExceedsLimit(let limit) = error else { + XCTFail("Unexpected error: \(error)") + return + } + XCTAssertEqual(limit, 1) + } + } + + func testWhenIOErrorHappens_itThrowsWhenReading() throws { + CreateTemporaryDirectory() + defer { DeleteTemporaryDirectory() } + + let url = temporaryDirectory.appendingPathComponent("file", isDirectory: false) + let stream = try XCTUnwrap(InputStream(url: url)) + let reader = BlockReader(input: stream) + + XCTAssertThrowsError(try reader.next()) { error in + guard let error = error as? TLVBlockError, case TLVBlockError.readOperationFailed(_, let nsError as NSError) = error else { + XCTFail("Unexpected error: \(error)") + return + } + XCTAssertTrue(nsError.localizedDescription.contains("No such file or directory")) + } + } +} + +private extension BlockReader { + convenience init(data: Data, maxBlockLength: UInt64? = nil) { + let stream = InputStream(data: data) + + if let maxBlockLength = maxBlockLength { + self.init(input: stream, maxBlockLength: maxBlockLength) + } else { + self.init(input: stream) + } + } +} diff --git a/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockTests.swift b/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockTests.swift new file mode 100644 index 0000000000..4bdb5f3771 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/TLV/TLVBlockTests.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 XCTest +import TestUtilities +@testable import DatadogCore + +private enum BlockType: UInt16 { + case one = 0x01 + case two = 0x02 + case three = 0x03 +} + +private typealias Block = TLVBlock + +class TLVBlockTests: XCTestCase { + func testSerializeBlock() throws { + XCTAssertEqual( + try Block(type: .one, data: Data([0xAA])).serialize(), + Data([0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xAA]) + // ^ type ^ ^ data size ^ ^data^ + ) + XCTAssertEqual( + try Block(type: .two, data: Data([0xAA, 0xBB])).serialize(), + Data([0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0xAA, 0xBB]) + ) + XCTAssertEqual( + try Block(type: .three, data: Data([0xAA, 0xBB, 0xCC])).serialize(), + Data([0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC]) + ) + } + + func testSerialize_zeroBytesBlock() throws { + XCTAssertEqual( + try Block(type: .one, data: Data()).serialize(), + Data([0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) + ) + } + + func testSerialize_largeBytesBlock() throws { + let largeData: Data = .mockRandom(ofSize: 10_000_000) // 10MB + let blockData = try Block(type: .one, data: largeData).serialize() + + XCTAssertEqual(blockData.count, 10_000_006) + // TLV representation: T=0x0000, L=0x00989680, V= + XCTAssertEqual(blockData.prefix(6), Data([0x01, 0x00, 0x80, 0x96, 0x98, 0x00])) + XCTAssertEqual(blockData.suffix(10_000_000), largeData) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift new file mode 100644 index 0000000000..0c0dc11865 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/AppBackgroundTaskCoordinatorTests.swift @@ -0,0 +1,69 @@ +/* + * 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 +@testable import DatadogCore + +class AppBackgroundTaskCoordinatorTests: XCTestCase { + var appSpy: AppSpy? + var coordinator: AppBackgroundTaskCoordinator? + + override func setUp() { + super.setUp() + appSpy = AppSpy() + coordinator = AppBackgroundTaskCoordinator( + app: appSpy + ) + } + + func testBeginBackgroundTask() { + coordinator?.beginBackgroundTask() + + XCTAssertEqual(appSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(appSpy?.endBackgroundTaskCalled, false) + } + + func testEndBackgroundTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.endBackgroundTask() + + XCTAssertEqual(appSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(appSpy?.endBackgroundTaskCalled, true) + } + + func testEndBackgroundTaskNotCalledWhenNotBegan() throws { + coordinator?.endBackgroundTask() + + XCTAssertEqual(appSpy?.beginBackgroundTaskCalled, false) + XCTAssertEqual(appSpy?.endBackgroundTaskCalled, false) + } + + func testBeginEndsPreviousTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.beginBackgroundTask() + + XCTAssertEqual(appSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(appSpy?.endBackgroundTaskCalled, true) + } +} + +class AppSpy: UIKitAppBackgroundTaskCoordinator { + var beginBackgroundTaskCalled = false + var endBackgroundTaskCalled = false + + var handler: (() -> Void)? = nil + + func beginBgTask(_ handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { + self.handler = handler + beginBackgroundTaskCalled = true + return UIBackgroundTaskIdentifier(rawValue: 1) + } + + func endBgTask(_ identifier: UIBackgroundTaskIdentifier) { + endBackgroundTaskCalled = true + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadConditionsTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadConditionsTests.swift new file mode 100644 index 0000000000..dca1b231f1 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadConditionsTests.swift @@ -0,0 +1,110 @@ +/* + * 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 +@testable import DatadogCore + +class DataUploadConditionsTests: XCTestCase { + private typealias Constants = DataUploadConditions.Constants + + func testItSaysToUploadOnCertainConditions() { + `repeat`(times: 100) { + assert( + canPerformUploadReturns: true, + forBattery: BatteryStatus( + state: .mockRandom(), level: Constants.minBatteryLevel + 0.01 + ), + isLowPowerModeEnabled: false, + forNetwork: .mockWith(reachability: .mockRandom(within: [.yes, .maybe])) + ) + assert( + canPerformUploadReturns: true, + forBattery: BatteryStatus( + state: .mockRandom(within: [.charging, .full]), level: .random(in: 0...100) + ), + isLowPowerModeEnabled: false, + forNetwork: .mockWith(reachability: .mockRandom(within: [.yes, .maybe])) + ) + assert( + canPerformUploadReturns: true, + forBattery: nil, + isLowPowerModeEnabled: false, + forNetwork: .mockWith(reachability: .mockRandom(within: [.yes, .maybe])) + ) + } + } + + func testItSaysToNotUploadOnCertainConditions() { + `repeat`(times: 100) { + assert( + canPerformUploadReturns: false, + forBattery: BatteryStatus( + state: .mockRandom(within: [.unplugged, .charging, .full]), level: .random(in: 0...100) + ), + isLowPowerModeEnabled: .random(), + forNetwork: .mockWith(reachability: .no) + ) + assert( + canPerformUploadReturns: false, + forBattery: BatteryStatus( + state: .mockRandom(within: [.unplugged, .charging, .full]), level: .random(in: 0...100) + ), + isLowPowerModeEnabled: true, + forNetwork: .mockWith(reachability: .mockRandom()) + ) + assert( + canPerformUploadReturns: false, + forBattery: BatteryStatus( + state: .unplugged, level: Constants.minBatteryLevel - 0.01 + ), + isLowPowerModeEnabled: .random(), + forNetwork: .mockWith(reachability: .mockRandom()) + ) + assert( + canPerformUploadReturns: false, + forBattery: nil, + isLowPowerModeEnabled: .random(), + forNetwork: .mockWith(reachability: .no) + ) + } + } + + func testItSaysToUploadIfTheBatteryStatusIsUnknown() { + `repeat`(times: 10) { + assert( + canPerformUploadReturns: true, + forBattery: BatteryStatus(state: .unknown, level: .random(in: -100...100)), + isLowPowerModeEnabled: .random(), + forNetwork: .mockWith(reachability: .mockRandom(within: [.yes, .maybe])) + ) + } + } + + private func assert( + canPerformUploadReturns value: Bool, + forBattery battery: BatteryStatus?, + isLowPowerModeEnabled: Bool, + forNetwork network: NetworkConnectionInfo, + file: StaticString = #file, + line: UInt = #line + ) { + let context: DatadogContext = .mockWith(networkConnectionInfo: network, batteryStatus: battery, isLowPowerModeEnabled: isLowPowerModeEnabled) + let conditions = DataUploadConditions() + let canPerformUpload = conditions.blockersForUpload(with: context).isEmpty + XCTAssertEqual( + value, + canPerformUpload, + "Expected `\(value)` but got `\(!value)` for:\n\(String(describing: battery)) and\n\(String(describing: network))", + file: file, + line: line + ) + } + + private func `repeat`(times: Int, block: () -> Void) { + (0.. mockPerformance.minUploadDelay { + delay.decrease() + + let nextValue = delay.current + XCTAssertEqual( + nextValue / previousValue, + 1.0 - mockPerformance.uploadDelayChangeRate, + accuracy: 0.1 + ) + XCTAssertLessThanOrEqual(nextValue, max(previousValue, mockPerformance.minUploadDelay)) + + previousValue = nextValue + } + } + + func testWhenIncreasing_itClampsToMaximumDelay() { + let delay = DataUploadDelay(performance: mockPerformance) + var previousValue: TimeInterval = delay.current + + while previousValue < mockPerformance.maxUploadDelay { + delay.increase() + + let nextValue = delay.current + XCTAssertEqual( + nextValue / previousValue, + 1.0 + mockPerformance.uploadDelayChangeRate, + accuracy: 0.1 + ) + XCTAssertGreaterThanOrEqual(nextValue, min(previousValue, mockPerformance.maxUploadDelay)) + previousValue = nextValue + } + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift new file mode 100644 index 0000000000..6e826828df --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadStatusTests.swift @@ -0,0 +1,161 @@ +/* + * 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 DatadogCore + +class DataUploadStatusTests: XCTestCase { + // MARK: - Test `.needsRetry` + + private let statusCodesExpectingNoRetry: [Int: String] = [ + 202: "accepted", + 400: "badRequest", + 401: "unauthorized", + 403: "forbidden", + 413: "payloadTooLarge", + ] + + 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(), 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(), 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.keys)) + + unexpectedStatusCodes.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 testWhenUploadFinishesWithError_itSetsNeedsRetryFlagToTrue() { + 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, message in + let requestID: String = .mockRandom(among: .alphanumerics) + 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, 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), attempt: 0) + XCTAssertEqual(status.userDebugDescription, "[error: \(randomErrorDescription)]") + } + + // MARK: - Test Upload Error + + private let alertingStatusCodes: Set = [ + 400, // BAD REQUEST + 401, // UNAUTHORIZED + 403, // FORBIDDEN + 413, // PAYLOAD TOO LARGE + 408, // REQUEST TIMEOUT + 429, // TOO MANY REQUESTS + ] + + func testWhenUploadFinishesWithResponse_andStatusCodeIs401_itCreatesError() { + 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, 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(), attempt: 01) + + guard case let .httpError(statusCode: receivedStatusCode) = status.error else { + return XCTFail("Upload status error should be created for status code: \(statusCode)") + } + + XCTAssertEqual(receivedStatusCode, statusCode) + } + } + + func testWhenUploadFinishesWithResponse_andStatusCodeMeansClientIssue_itDoesNotCreateHTTPError() { + let clientIssueStatusCodes = Set(expectedStatusCodes.keys).subtracting(Set(alertingStatusCodes)) + clientIssueStatusCodes.forEach { statusCode in + 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.keys)) + unexpectedStatusCodes.forEach { statusCode in + 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), attempt: 0) + + guard case let .networkError(error: nserror) = status.error else { + return XCTFail("Upload status error should be created for NSURLError code: \(alertingNSURLErrorCode)") + } + + XCTAssertEqual(nserror.code, alertingNSURLErrorCode) + } + + func testWhenUploadFinishesWithError_andErrorCodeMeansExternalFactors_itDoesNotCreateNetworkError() { + let notAlertingNSURLErrorCode = NSURLErrorNetworkConnectionLost + 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)") + } + + // MARK: - Test Response Code + + func testWhenUploadFinishesWithResponse_itSetsResponseCode() { + let randomCode: Int = .mockRandom(min: 1, max: 999) + 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 new file mode 100644 index 0000000000..f7ae3a6f8e --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -0,0 +1,813 @@ +/* + * 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 +@testable import DatadogCore + +class DataUploadWorkerTests: XCTestCase { + private let uploaderQueue = DispatchQueue(label: "dd-tests-uploader", target: .global(qos: .utility)) + + lazy var dateProvider = RelativeDateProvider(advancingBySeconds: 1) + lazy var orchestrator = FilesOrchestrator( + directory: .init(url: temporaryDirectory), + performance: StoragePerformanceMock.writeEachObjectToNewFileAndReadAllFiles, + dateProvider: dateProvider, + telemetry: NOPTelemetry() + ) + lazy var writer = FileWriter( + orchestrator: orchestrator, + encryption: nil, + telemetry: NOPTelemetry() + ) + lazy var reader = FileReader( + orchestrator: orchestrator, + encryption: nil, + telemetry: NOPTelemetry() + ) + + override func setUp() { + super.setUp() + CreateTemporaryDirectory() + } + + override func tearDown() { + DeleteTemporaryDirectory() + super.tearDown() + } + + // MARK: - Data Uploads + + func testItUploadsAllData() { + let uploadExpectation = self.expectation(description: "Make 3 uploads") + uploadExpectation.expectedFulfillmentCount = 3 + + let dataUploader = DataUploaderMock( + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } + ) + + // Given + writer.write(value: ["k1": "v1"]) + writer.write(value: ["k2": "v2"]) + writer.write(value: ["k3": "v3"]) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: dataUploader, + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: 1 + ) + + // Then + waitForExpectations(timeout: 1) + XCTAssertEqual(dataUploader.uploadedEvents[0], Event(data: #"{"k1":"v1"}"#.utf8Data)) + XCTAssertEqual(dataUploader.uploadedEvents[1], Event(data: #"{"k2":"v2"}"#.utf8Data)) + XCTAssertEqual(dataUploader.uploadedEvents[2], Event(data: #"{"k3":"v3"}"#.utf8Data)) + + worker.cancelSynchronously() + XCTAssertEqual(try orchestrator.directory.files().count, 0) + } + + func testItUploadsDataSequentiallyWithoutDelay_whenMaxBatchesPerUploadIsSet() { + let uploadExpectation = self.expectation(description: "Make 2 uploads") + uploadExpectation.expectedFulfillmentCount = 2 + + let dataUploader = DataUploaderMock( + uploadStatus: DataUploadStatus( + httpResponse: .mockResponseWith(statusCode: 200), + ddRequestID: nil, + attempt: 0 + ), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } + ) + + // Given + writer.write(value: ["k1": "v1"]) + writer.write(value: ["k2": "v2"]) + writer.write(value: ["k3": "v3"]) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: dataUploader, + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: 2 + ) + + // Then + waitForExpectations(timeout: 1) + XCTAssertEqual(dataUploader.uploadedEvents.count, 2) + XCTAssertEqual(dataUploader.uploadedEvents[0], Event(data: #"{"k1":"v1"}"#.utf8Data)) + XCTAssertEqual(dataUploader.uploadedEvents[1], Event(data: #"{"k2":"v2"}"#.utf8Data)) + + worker.cancelSynchronously() + XCTAssertEqual(try orchestrator.directory.files().count, 1) + } + + func testGivenDataToUpload_whenUploadFinishesAndDoesNotNeedToBeRetried_thenDataIsDeleted() { + let startUploadExpectation = self.expectation(description: "Upload has started") + + let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: false)) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + 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.veryQuickInitialUpload), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try orchestrator.directory.files().count, 0, "When upload finishes with `needsRetry: false`, data should be deleted") + } + + func testGivenDataToUpload_whenUploadFailsToBeInitiated_thenDataIsDeleted() { + let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") + + let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + initiatingUploadExpectation.fulfill() + throw ErrorMock("Failed to prepare upload") + } + + // 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.veryQuickInitialUpload), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [initiatingUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(try orchestrator.directory.files().count, 0, "When upload fails to be initiated, data should be deleted") + } + + func testGivenDataToUpload_whenUploadFinishesAndNeedsToBeRetried_thenDataIsPreserved() { + let startUploadExpectation = self.expectation(description: "Upload has started") + + let mockDataUploader = DataUploaderMock(uploadStatus: .mockWith(needsRetry: true)) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + 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.veryQuickInitialUpload), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + 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() { + let delayChangeExpectation = expectation(description: "Upload delay is increased") + let initialUploadDelay = 0.01 + let delay = DataUploadDelay( + performance: UploadPerformanceMock( + initialUploadDelay: initialUploadDelay, + minUploadDelay: 0, + maxUploadDelay: 1, + uploadDelayChangeRate: 0.01 + ) + ) + + // When + XCTAssertEqual(try orchestrator.directory.files().count, 0) + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: DataUploaderMock(uploadStatus: .mockWith()), + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.neverUpload(), + delay: delay, + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + // Then + wait(until: { [uploaderQueue] in + uploaderQueue.sync { + delay.current > initialUploadDelay + } + }, andThenFulfill: delayChangeExpectation) + wait(for: [delayChangeExpectation], timeout: 0.5) + worker.cancelSynchronously() + } + + func testWhenBatchFails_thenIntervalIncreasesAndUploadCycleEnds() { + let delayChangeExpectation = expectation(description: "Upload delay is increased") + delayChangeExpectation.expectedFulfillmentCount = 1 + + let uploadAttemptExpectation = expectation(description: "Upload was attempted") + uploadAttemptExpectation.expectedFulfillmentCount = 1 + + let initialUploadDelay = 0.01 + let delay = DataUploadDelay( + performance: UploadPerformanceMock( + initialUploadDelay: initialUploadDelay, + minUploadDelay: 0, + maxUploadDelay: 1, + uploadDelayChangeRate: 0.01 + ) + ) + + // When + writer.write(value: ["k1": "v1"]) + writer.write(value: ["k2": "v2"]) + writer.write(value: ["k3": "v3"]) + + let dataUploader = DataUploaderMock( + uploadStatus: .mockWith( + needsRetry: true, + error: .httpError(statusCode: 500) + ) + ) { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadAttemptExpectation.fulfill() + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: dataUploader, + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.alwaysUpload(), + delay: delay, + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + // Then + wait(until: { [uploaderQueue] in + uploaderQueue.sync { + delay.current > initialUploadDelay + } + }, andThenFulfill: delayChangeExpectation) + wait(for: [delayChangeExpectation, uploadAttemptExpectation], timeout: 0.5) + worker.cancelSynchronously() + } + + func testWhenBatchSucceeds_thenIntervalDecreases() { + let delayChangeExpectation = expectation(description: "Upload delay is decreased") + let initialUploadDelay = 0.05 + let delay = DataUploadDelay( + performance: UploadPerformanceMock( + initialUploadDelay: initialUploadDelay, + minUploadDelay: 0, + maxUploadDelay: 1, + uploadDelayChangeRate: 0.01 + ) + ) + // When + writer.write(value: ["k1": "v1"]) + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: DataUploaderMock(uploadStatus: .mockWith(needsRetry: false)), + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.alwaysUpload(), + delay: delay, + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + // Then + wait(until: { [uploaderQueue] in + uploaderQueue.sync { + delay.current < initialUploadDelay + } + }, andThenFulfill: delayChangeExpectation) + wait(for: [delayChangeExpectation], timeout: 0.5) + worker.cancelSynchronously() + } + + // MARK: - Notifying Upload Progress + + func testWhenDataIsBeingUploaded_itPrintsUploadProgressInformation() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + // Given + writer.write(value: ["key": "value"]) + + let randomUploadStatus: DataUploadStatus = .mockRandom() + let randomFeatureName: String = .mockRandom() + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: randomFeatureName, + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + let expectedSummary = randomUploadStatus.needsRetry ? "not delivered, will be retransmitted" : "accepted, won't be retransmitted" + XCTAssertEqual(dd.logger.debugLogs.count, 2) + + XCTAssertEqual( + dd.logger.debugLogs[0].message, + "⏳ (\(randomFeatureName)) Uploading batches...", + "Batch start information should be printed to `userLogger`. All captured logs:\n\(dd.logger.recordedLogs)" + ) + + XCTAssertEqual( + dd.logger.debugLogs[1].message, + " → (\(randomFeatureName)) \(expectedSummary): \(randomUploadStatus.userDebugDescription)", + "Batch completion information should be printed to `userLogger`. All captured logs:\n\(dd.logger.recordedLogs)" + ) + } + + func testWhenDataIsUploadedWithUnauthorizedError_itPrintsUnauthoriseMessage_toUserLogger() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + // Given + writer.write(value: ["key": "value"]) + + let randomUploadStatus: DataUploadStatus = .mockWith(error: .unauthorized) + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: .mockRandom(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual( + dd.logger.errorLog?.message, + "⚠️ Make sure that the provided token still exists and you're targeting the relevant Datadog site.", + "An error should be printed to `userLogger`. All captured logs:\n\(dd.logger.recordedLogs)" + ) + } + + func testWhenDataIsUploadedWith500StatusCode_itSendsErrorTelemetry() throws { + // Given + let telemetry = TelemetryMock() + + writer.write(value: ["key": "value"]) + let randomUploadStatus: DataUploadStatus = .mockWith(error: .httpError(statusCode: 500)) + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: .mockRandom(), + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(telemetry.messages.count, 1) + + let error = try XCTUnwrap(telemetry.messages.first?.asError, "An error should be send to `telemetry`.") + XCTAssertEqual(error.message,"Data upload finished with status code: 500") + } + + func testWhenDataCannotBeUploadedDueToNetworkError_itSendsErrorTelemetry() throws { + // Given + let telemetry = TelemetryMock() + + writer.write(value: ["key": "value"]) + let randomUploadStatus: DataUploadStatus = .mockWith(error: .networkError(error: .mockAny())) + + // When + let startUploadExpectation = self.expectation(description: "Upload has started") + let mockDataUploader = DataUploaderMock(uploadStatus: randomUploadStatus) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + startUploadExpectation.fulfill() + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: .mockRandom(), + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [startUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(telemetry.messages.count, 1) + + let error = try XCTUnwrap(telemetry.messages.first?.asError, "An error should be send to `telemetry`.") + XCTAssertEqual(error.message, #"Data upload finished with error - Error Domain=abc Code=0 "(null)""#) + } + + func testWhenDataCannotBePreparedForUpload_itSendsErrorTelemetry() throws { + // Given + let telemetry = TelemetryMock() + + writer.write(value: ["key": "value"]) + + // When + let initiatingUploadExpectation = self.expectation(description: "Upload is being initiated") + let mockDataUploader = DataUploaderMock(uploadStatus: .mockRandom()) + mockDataUploader.onUpload = { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + initiatingUploadExpectation.fulfill() + throw ErrorMock("Failed to prepare upload") + } + + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: mockDataUploader, + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: "some-feature", + telemetry: telemetry, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + wait(for: [initiatingUploadExpectation], timeout: 0.5) + worker.cancelSynchronously() + + // Then + XCTAssertEqual(telemetry.messages.count, 1) + + let error = try XCTUnwrap(telemetry.messages.first?.asError, "An error should be send to `telemetry`.") + XCTAssertEqual(error.message, #"Failed to initiate 'some-feature' data upload - Failed to prepare upload"#) + } + + // MARK: - Tearing Down + + func testWhenCancelled_itPerformsNoMoreUploads() { + // Given + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let httpClient = URLSessionClient(session: server.getInterceptedURLSession()) + + let dataUploader = DataUploader( + httpClient: httpClient, + requestBuilder: FeatureRequestBuilderMock() + ) + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: dataUploader, + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.neverUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + // When + worker.cancelSynchronously() + + // Then + writer.write(value: ["k1": "v1"]) + + server.waitFor(requestsCompletion: 0) + } + + func testItFlushesAllData() { + let uploadExpectation = self.expectation(description: "Make 3 uploads") + uploadExpectation.expectedFulfillmentCount = 3 + + let dataUploader = DataUploaderMock( + uploadStatus: .mockWith(needsRetry: false), + onUpload: { previousUploadStatus in + XCTAssertNil(previousUploadStatus) + uploadExpectation.fulfill() + } + ) + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: dataUploader, + contextProvider: .mockAny(), + uploadConditions: DataUploadConditions.alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100) + ) + + // Given + writer.write(value: ["k1": "v1"]) + writer.write(value: ["k2": "v2"]) + writer.write(value: ["k3": "v3"]) + + // When + worker.flushSynchronously() + + // Then + XCTAssertEqual(try orchestrator.directory.files().count, 0) + + waitForExpectations(timeout: 1) + XCTAssertEqual(dataUploader.uploadedEvents[0], Event(data: #"{"k1":"v1"}"#.utf8Data)) + XCTAssertEqual(dataUploader.uploadedEvents[1], Event(data: #"{"k2":"v2"}"#.utf8Data)) + XCTAssertEqual(dataUploader.uploadedEvents[2], Event(data: #"{"k3":"v3"}"#.utf8Data)) + + worker.cancelSynchronously() + } + + func testItTriggersBackgroundTaskBeginEndForSuccessfulUpload() { + let expectTaskRegistered = expectation(description: "task should be registered") + let expectTaskEnded = expectation(description: "task should be ended") + let backgroundTaskCoordinator = SpyBackgroundTaskCoordinator( + beginBackgroundTaskCalled: { + expectTaskRegistered.fulfill() + }, endBackgroundTaskCalled: { + expectTaskEnded.fulfill() + } + ) + + // Given + writer.write(value: ["k1": "v1"]) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: DataUploaderMock(uploadStatus: .mockWith()), + contextProvider: .mockAny(), + uploadConditions: .alwaysUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTaskCoordinator: backgroundTaskCoordinator + ) + + // Then + withExtendedLifetime(worker) { + wait(for: [expectTaskRegistered, expectTaskEnded], timeout: 0.5) + } + } + + func testItTriggersBackgroundTaskBeginEndWhenBlockerOccurs() { + let expectTaskRegistered = expectation(description: "task should be registered") + let expectTaskEnded = expectation(description: "task should be ended") + let backgroundTaskCoordinator = SpyBackgroundTaskCoordinator( + beginBackgroundTaskCalled: { + expectTaskRegistered.fulfill() + }, endBackgroundTaskCalled: { + expectTaskEnded.fulfill() + } + ) + + // Given + writer.write(value: ["k1": "v1"]) + + // When + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: DataUploaderMock(uploadStatus: .mockWith()), + contextProvider: .mockAny(), + uploadConditions: .neverUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTaskCoordinator: backgroundTaskCoordinator + ) + + // Then + withExtendedLifetime(worker) { + wait(for: [expectTaskRegistered, expectTaskEnded], timeout: 0.5) + } + } + + func testItTriggersBackgroundTaskEndWhenThereIsNothingToUpload() { + let expectTaskEnded = expectation(description: "task should be ended") + let backgroundTaskCoordinator = SpyBackgroundTaskCoordinator( + beginBackgroundTaskCalled: { + XCTFail("begin background task should not be called") + }, endBackgroundTaskCalled: { + expectTaskEnded.fulfill() + } + ) + let worker = DataUploadWorker( + queue: uploaderQueue, + fileReader: reader, + dataUploader: DataUploaderMock(uploadStatus: .mockWith()), + contextProvider: .mockAny(), + uploadConditions: .neverUpload(), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuickInitialUpload), + featureName: .mockAny(), + telemetry: NOPTelemetry(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTaskCoordinator: backgroundTaskCoordinator + ) + // Then + withExtendedLifetime(worker) { + wait(for: [expectTaskEnded], timeout: 0.5) + } + } +} + +private extension DataUploadConditions { + static func alwaysUpload() -> DataUploadConditions { + return DataUploadConditions(minBatteryLevel: 0) + } + + static func neverUpload() -> DataUploadConditions { + return DataUploadConditions(minBatteryLevel: 1) + } +} + +private class SpyBackgroundTaskCoordinator: BackgroundTaskCoordinator { + private let beginBackgroundTaskCalled: () -> Void + private let endBackgroundTaskCalled: () -> Void + + init( + beginBackgroundTaskCalled: @escaping () -> Void, + endBackgroundTaskCalled: @escaping () -> Void + ) { + self.beginBackgroundTaskCalled = beginBackgroundTaskCalled + self.endBackgroundTaskCalled = endBackgroundTaskCalled + } + + func beginBackgroundTask() { + beginBackgroundTaskCalled() + } + + func endBackgroundTask() { + endBackgroundTaskCalled() + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift new file mode 100644 index 0000000000..d8884c70c7 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/DataUploaderTests.swift @@ -0,0 +1,83 @@ +/* + * 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 +import TestUtilities +@testable import DatadogCore + +class DataUploaderTests: XCTestCase { + // swiftlint:disable opening_brace + func testGivenValidRequest_whenUploadCompletesWithStatusCode_itReturnsUploadStatus() throws { + // Given + let randomResponse: HTTPURLResponse = .mockResponseWith(statusCode: (100...599).randomElement()!) + let randomRequest: URLRequest = oneOf([ + { .mockWith(headers: [:]) }, + { .mockWith(headers: ["DD-REQUEST-ID": String.mockRandom()]) } + ]) + + let uploader = DataUploader( + httpClient: HTTPClientMock(response: randomResponse), + requestBuilder: FeatureRequestBuilderMock(request: randomRequest) + ) + + // When + let uploadStatus = try uploader.upload( + events: .mockAny(), + context: .mockAny(), + previous: nil + ) + + // Then + let expectedUploadStatus = DataUploadStatus( + httpResponse: randomResponse, + ddRequestID: randomRequest.value(forHTTPHeaderField: "DD-REQUEST-ID"), + attempt: 0 + ) + + DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) + } + // swiftlint:enable opening_brace + + func testGivenValidRequest_whenUploadCompletesWithError_itReturnsUploadStatus() throws { + // Given + let randomErrorDescription: String = .mockRandom() + let randomError = NSError(domain: .mockRandom(), code: .mockRandom(), userInfo: [NSLocalizedDescriptionKey: randomErrorDescription]) + let randomRequest: URLRequest = .mockAny() + + let uploader = DataUploader( + httpClient: HTTPClientMock(error: randomError), + requestBuilder: FeatureRequestBuilderMock(request: randomRequest) + ) + + // When + let uploadStatus = try uploader.upload( + events: .mockAny(), + context: .mockAny(), + previous: nil + ) + + // Then + let expectedUploadStatus = DataUploadStatus(networkError: randomError, attempt: 0) + + DDAssertReflectionEqual(uploadStatus, expectedUploadStatus) + } + + func testWhenRequestCannotBeCreated_itThrows() throws { + // Given + let error = ErrorMock() + + let uploader = DataUploader( + httpClient: HTTPClientMock(), + requestBuilder: FailingRequestBuilderMock(error: error) + ) + + // When & Then + XCTAssertThrowsError(try uploader.upload(events: .mockAny(), context: .mockAny(), previous: nil)) { error in + XCTAssertTrue(error is ErrorMock) + } + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift new file mode 100644 index 0000000000..ca31127a20 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/ExtensionBackgroundTaskCoordinatorTests.swift @@ -0,0 +1,66 @@ +/* + * 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 +@testable import DatadogCore + +class ExtensionBackgroundTaskCoordinatorTests: XCTestCase { + var processInfoSpy: ProcessInfoSpy! // swiftlint:disable:this implicitly_unwrapped_optional + var coordinator: ExtensionBackgroundTaskCoordinator! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + processInfoSpy = ProcessInfoSpy() + coordinator = ExtensionBackgroundTaskCoordinator( + processInfo: processInfoSpy + ) + } + + func testBeginBackgroundTask() { + coordinator?.beginBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, false) + } + + func testEndBackgroundTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.endBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, true) + } + + func testEndBackgroundTaskNotCalledWhenNotBegan() throws { + coordinator?.endBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, false) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, false) + } + + func testBeginEndsPreviousTask() throws { + coordinator?.beginBackgroundTask() + coordinator?.beginBackgroundTask() + + XCTAssertEqual(processInfoSpy?.beginBackgroundTaskCalled, true) + XCTAssertEqual(processInfoSpy?.endBackgroundTaskCalled, true) + } +} + +class ProcessInfoSpy: ProcessInfoActivityCoordinator { + var beginBackgroundTaskCalled = false + var endBackgroundTaskCalled = false + + func beginActivity(options: ProcessInfo.ActivityOptions, reason: String) -> any NSObjectProtocol { + beginBackgroundTaskCalled = true + return NSObject() + } + + func endActivity(_ activity: any NSObjectProtocol) { + endBackgroundTaskCalled = true + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/RequestBuilderTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/RequestBuilderTests.swift new file mode 100644 index 0000000000..cfd5208b65 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/RequestBuilderTests.swift @@ -0,0 +1,217 @@ +/* + * 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 +@testable import DatadogCore + +class RequestBuilderTests: XCTestCase { + // MARK: - Request URL + + func testBuildingRequestWithURLAndQueryItems() throws { + let randomURL: URL = .mockRandom() + let builder = URLRequestBuilder( + url: randomURL, + queryItems: [.ddsource(source: "abc"), .ddtags(tags: ["abc:def"])], + headers: .mockRandom() + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.url?.absoluteString, "\(randomURL.absoluteString)?ddsource=abc&ddtags=abc:def") + } + + func testWhenBuildingRequestWithURLAndQueryItems_itEscapesWhitespacesInQuery() throws { + let randomURL: URL = .mockRandom() + let builder = URLRequestBuilder( + url: randomURL, + queryItems: [.ddsource(source: "source with whitespace"), .ddtags(tags: ["tag with whitespace"])], + headers: .mockRandom() + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.url?.absoluteString, "\(randomURL.absoluteString)?ddsource=source%20with%20whitespace&ddtags=tag%20with%20whitespace") + } + + // MARK: - Request Headers + + func testBuildingRequestWithContentTypeHeader() { + var builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.contentTypeHeader(contentType: .textPlainUTF8)]) + var request = builder.uploadRequest(with: .mockAny()) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "text/plain;charset=UTF-8") + + builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.contentTypeHeader(contentType: .applicationJSON)]) + request = builder.uploadRequest(with: .mockAny()) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") + + builder = URLRequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [.contentTypeHeader(contentType: .multipartFormData(boundary: "boundary-uuid"))] + ) + request = builder.uploadRequest(with: .mockAny()) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "multipart/form-data; boundary=boundary-uuid") + } + + func testBuildingRequestWithUserAgentHeader() { + let builder = URLRequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [ + .userAgentHeader( + appName: "FoobarApp", + appVersion: "1.2.3", + device: .mockWith( + name: "iPhone", + osName: "iOS", + osVersion: "13.3.1" + ) + ) + ] + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)") + } + + func testBuildingRequestWithComplexUserAgentHeader() { + let builder = URLRequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [ + .userAgentHeader( + appName: "Foobar 電話 𝛼β", + appVersion: "1.2.3", + device: .mockWith( + name: "iPhone", + osName: "iOS", + osVersion: "13.3.1" + ) + ) + ] + ) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "Foobar/1.2.3 CFNetwork (iPhone; iOS/13.3.1)") + } + + func testBuildingRequestWithDDAPIKeyHeader() { + let randomClientToken: String = .mockRandom() + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddAPIKeyHeader(clientToken: randomClientToken)]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + } + + func testBuildingRequestWithDDEVPOriginHeader() { + let randomSource: String = .mockRandom() + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddEVPOriginHeader(source: randomSource)]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomSource) + } + + func testBuildingRequestWithDDEVPOriginVersionHeader() { + let randomSDKVersion: String = .mockRandom() + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddEVPOriginVersionHeader(sdkVersion: randomSDKVersion)]) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], randomSDKVersion) + } + + func testBuildingRequestWithDDRequestIDHeader() throws { + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: [.ddRequestIDHeader()]) + + let request1 = builder.uploadRequest(with: .mockRandom()) + let request2 = builder.uploadRequest(with: .mockRandom()) + let request3 = builder.uploadRequest(with: .mockRandom()) + + let requestID1 = try XCTUnwrap(request1.allHTTPHeaderFields?["DD-REQUEST-ID"]) + let requestID2 = try XCTUnwrap(request2.allHTTPHeaderFields?["DD-REQUEST-ID"]) + let requestID3 = try XCTUnwrap(request3.allHTTPHeaderFields?["DD-REQUEST-ID"]) + + let allIDs = Set([requestID1, requestID2, requestID3]) + XCTAssertEqual(allIDs.count, 3, "Each `DD-REQUEST-ID` must produce unique ID") + allIDs.forEach { id in + XCTAssertTrue(id.matches(regex: .uuidRegex), "Each `DD-REQUEST-ID` must be an UUID string") + } + } + + func testBuildingRequestWithMultipleHeaders() { + let builder = URLRequestBuilder( + url: .mockRandom(), + queryItems: .mockRandom(), + headers: [ + .contentTypeHeader(contentType: .textPlainUTF8), + .userAgentHeader(appName: .mockAny(), appVersion: .mockAny(), device: .mockAny()), + .ddAPIKeyHeader(clientToken: .mockAny()), + .ddEVPOriginHeader(source: .mockAny()), + .ddEVPOriginVersionHeader(sdkVersion: .mockAny()), + .ddRequestIDHeader(), + ] + ) + + let request = builder.uploadRequest(with: .mockAny()) + XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Type"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Encoding"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["User-Agent"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-API-KEY"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"]) + XCTAssertNotNil(request.allHTTPHeaderFields?["DD-REQUEST-ID"]) + XCTAssertEqual(request.allHTTPHeaderFields?.count, 7) + } + + // MARK: - Request Method + + func testItUsesPOSTMethodForProducedReqest() { + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + let request = builder.uploadRequest(with: .mockRandom()) + XCTAssertEqual(request.httpMethod, "POST") + } + + // MARK: - Request Data + + func testWhenBuildingRequestWithDataAndCompression_thenItDeflatesHTTPBody() throws { + // Given + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + + for i in 2...8 { // Test from 100KB to 100MB + // When + let size = UInt64(pow(10, Double(i))) + let randomData: Data = .mock(ofSize: size) + let request = builder.uploadRequest(with: randomData, compress: true) + let body = try XCTUnwrap(request.httpBody) + + // Then + XCTAssertNotNil(request.allHTTPHeaderFields?["Content-Encoding"]) + XCTAssertLessThan(body.count, Int(size), "HTTP body must be compressed") + } + } + + func testWhenBuildingRequestWithSmallDataAndCompression_thenItDoesNotDeflateHTTPBody() throws { + // When + // In the worst possible case, where deflate would expand the data, + // deflation falls back to stored (uncompressed) data. + let size = 8 // Small data will most likely inflate with zlib. + let randomData: Data = .mock(ofSize: size) + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + + let request = builder.uploadRequest(with: randomData, compress: true) + let body = try XCTUnwrap(request.httpBody) + + // Then + XCTAssertNil(request.allHTTPHeaderFields?["Content-Encoding"]) + XCTAssertEqual(body.count, Int(size), "HTTP body must not be alterated") + XCTAssertEqual(body, randomData) + } + + func testWhenBuildingRequestWithDataAndNoCompression_thenItDoesNotDeflatesHTTPBody() throws { + // Given + let builder = URLRequestBuilder(url: .mockRandom(), queryItems: .mockRandom(), headers: .mockRandom()) + + // When + let randomData: Data = .mockRandom() + let request = builder.uploadRequest(with: randomData, compress: false) + + // Then + let body = try XCTUnwrap(request.httpBody) + XCTAssertNil(request.allHTTPHeaderFields?["Content-Encoding"]) + XCTAssertEqual(body, randomData, "HTTP body must not be compressed") + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Upload/URLSessionClientTests.swift b/DatadogCore/Tests/Datadog/Core/Upload/URLSessionClientTests.swift new file mode 100644 index 0000000000..f3fda0cd24 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Upload/URLSessionClientTests.swift @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +@testable import DatadogCore + +class URLSessionClientTests: XCTestCase { + func testWhenRequestIsDelivered_itReturnsHTTPResponse() { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let expectation = self.expectation(description: "receive response") + let client = URLSessionClient(session: server.getInterceptedURLSession()) + + client.send(request: .mockAny()) { result in + switch result { + case .success(let httpResponse): + XCTAssertEqual(httpResponse.statusCode, 200) + expectation.fulfill() + case .failure: + break + } + } + + waitForExpectations(timeout: 1, handler: nil) + server.waitFor(requestsCompletion: 1) + } + + func testWhenRequestIsNotDelivered_itReturnsHTTPRequestDeliveryError() { + let mockError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "no internet connection"]) + let server = ServerMock(delivery: .failure(error: mockError)) + let expectation = self.expectation(description: "receive response") + let client = URLSessionClient(session: server.getInterceptedURLSession()) + + client.send(request: .mockAny()) { result in + switch result { + case .success: + break + case .failure(let error): + XCTAssertEqual((error as NSError).localizedDescription, "no internet connection") + expectation.fulfill() + } + } + + waitForExpectations(timeout: 1, handler: nil) + server.waitFor(requestsCompletion: 1) + } + + func testWhenProxyConfigurationIsSet_itUsesProxyConfiguration() { + let proxyConfiguration: [AnyHashable: Any] = [ + kCFNetworkProxiesHTTPEnable: true, + kCFNetworkProxiesHTTPPort: 123, + kCFNetworkProxiesHTTPProxy: "www.example.com", + kCFProxyUsernameKey: "proxyuser", + kCFProxyPasswordKey: "proxypass", + ] + + let client = URLSessionClient(proxyConfiguration: proxyConfiguration) + + let actualProxy: [AnyHashable: Any] = client.session.configuration.connectionProxyDictionary! + XCTAssertEqual(actualProxy[kCFNetworkProxiesHTTPEnable] as? Bool, true) + XCTAssertEqual(actualProxy[kCFNetworkProxiesHTTPPort] as? Int, 123) + XCTAssertEqual(actualProxy[kCFNetworkProxiesHTTPProxy] as? String, "www.example.com") + XCTAssertEqual(actualProxy[kCFProxyUsernameKey] as? String, "proxyuser") + XCTAssertEqual(actualProxy[kCFProxyPasswordKey] as? String, "proxypass") + XCTAssertEqual( + client.session.configuration.httpAdditionalHeaders?["Proxy-Authorization"] as? String, + "Basic cHJveHl1c2VyOnByb3h5cGFzcw==" // Base64.encode(proxyuser:proxypass) + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Utils/CryptographyTests.swift b/DatadogCore/Tests/Datadog/Core/Utils/CryptographyTests.swift new file mode 100644 index 0000000000..2da3b10056 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Utils/CryptographyTests.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 XCTest +@testable import DatadogCore + +class CryptographyTests: XCTestCase { + func testItComputesSHA256ForArbitraryString() { + (0..<20).forEach { _ in + // Given + let string: String = .mockRandom(among: .allUnicodes, length: .mockRandom(min: 1, max: 500)) + + // When + let sha = sha256(string) + + // Then + XCTAssertEqual(sha.count, 64, "It must use 64 characters") + XCTAssertFalse(sha.contains(where: { !$0.isASCII }), "It must contain only ASCII characters") + } + } + + func testWhenComputingSHA256_itGivesStableResults() { + XCTAssertEqual(sha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + XCTAssertEqual(sha256("foo"), "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae") + XCTAssertEqual(sha256("Bar Bizz"), "ad81f738a40902ef4bb3e4d5f5b83d3e2b3b7edfcd1669ddd4004d4815d03a75") + XCTAssertEqual(sha256(".//,"), "822236197264817e14fdd2939d9dc68c7d0151a6265798dd29511315cb428c66") + } +} diff --git a/DatadogCore/Tests/Datadog/Core/Utils/DDErrorTests.swift b/DatadogCore/Tests/Datadog/Core/Utils/DDErrorTests.swift new file mode 100644 index 0000000000..3afd21a358 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Utils/DDErrorTests.swift @@ -0,0 +1,96 @@ +/* + * 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 +@testable import DatadogCore + +class DDErrorTests: XCTestCase { + func testFormattingBasicSwiftError() { + struct SwiftError: Error { + let description = "error description" + } + + let error = SwiftError() + let dderror = DDError(error: error) + + XCTAssertEqual(dderror.type, "SwiftError") + XCTAssertEqual(dderror.message, #"SwiftError(description: "error description")"#) + XCTAssertEqual(dderror.stack, #"SwiftError(description: "error description")"#) + } + + func testFormattingStringConvertibleSwiftError() { + struct SwiftError: Error, CustomDebugStringConvertible { + let debugDescription = "error description" + } + + let error = SwiftError() + let dderror = DDError(error: error) + + XCTAssertEqual(dderror.type, "SwiftError") + XCTAssertEqual(dderror.message, "error description") + XCTAssertEqual(dderror.stack, "error description") + } + + func testFormattingNSError() { + let error = NSError( + domain: "custom-domain", + code: 10, + userInfo: [ + NSLocalizedDescriptionKey: "error description" + ] + ) + let dderror = DDError(error: error) + + XCTAssertEqual(dderror.type, "custom-domain - 10") + XCTAssertEqual(dderror.message, "error description") + XCTAssertEqual( + dderror.stack, + """ + Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description} + """ + ) + } + + func testFormattingNSErrorSubclass() { + class NSErrorSubclass: NSError {} + + let dderrorWithDescription = DDError( + error: NSErrorSubclass( + domain: "custom-domain", + code: 10, + userInfo: [NSLocalizedDescriptionKey: "localized description"] + ) + ) + + XCTAssertEqual(dderrorWithDescription.type, "custom-domain - 10") + XCTAssertEqual(dderrorWithDescription.message, "localized description") + XCTAssertEqual( + dderrorWithDescription.stack, + """ + Error Domain=custom-domain Code=10 "localized description" UserInfo={NSLocalizedDescription=localized description} + """ + ) + + let dderrorNoDescription = DDError( + error: NSErrorSubclass( + domain: "custom-domain", + code: 10, + userInfo: [:] + ) + ) + + XCTAssertEqual(dderrorNoDescription.type, "custom-domain - 10") + XCTAssertEqual( + dderrorNoDescription.message, + #"Error Domain=custom-domain Code=10 "(null)""# + ) + XCTAssertEqual( + dderrorNoDescription.stack, + #"Error Domain=custom-domain Code=10 "(null)""# + ) + } +} diff --git a/Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift b/DatadogCore/Tests/Datadog/Core/Utils/DateFormattingTests.swift similarity index 90% rename from Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift rename to DatadogCore/Tests/Datadog/Core/Utils/DateFormattingTests.swift index c238e287eb..417d710584 100644 --- a/Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Utils/DateFormattingTests.swift @@ -1,11 +1,12 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import XCTest -@testable import Datadog +import DatadogInternal +@testable import DatadogCore class DateFormattingTests: XCTestCase { private let date: Date = .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.001) diff --git a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift b/DatadogCore/Tests/Datadog/Core/Utils/JSONEncoderTests.swift similarity index 85% rename from Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift rename to DatadogCore/Tests/Datadog/Core/Utils/JSONEncoderTests.swift index 5e4fe43f8f..650b08df52 100644 --- a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift +++ b/DatadogCore/Tests/Datadog/Core/Utils/JSONEncoderTests.swift @@ -1,14 +1,17 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import XCTest -@testable import Datadog +import TestUtilities +import DatadogInternal + +@testable import DatadogCore class JSONEncoderTests: XCTestCase { - private let jsonEncoder = JSONEncoder.default() + private let jsonEncoder = JSONEncoder.dd.default() func testDateEncoding() throws { let encodedDate = try jsonEncoder.encode( diff --git a/DatadogCore/Tests/Datadog/Core/Utils/RetryingTests.swift b/DatadogCore/Tests/Datadog/Core/Utils/RetryingTests.swift new file mode 100644 index 0000000000..14f0f59eac --- /dev/null +++ b/DatadogCore/Tests/Datadog/Core/Utils/RetryingTests.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 XCTest +import TestUtilities +@testable import DatadogCore + +private class OperationMock { + private let succeedingCallResults: [Result] + private(set) var callsReceived = 0 + + init(_ succeedingCallResults: [Result]) { + self.succeedingCallResults = succeedingCallResults + } + + func operation() throws -> Int { + let result = succeedingCallResults[callsReceived] + callsReceived += 1 + return try result.get() + } +} + +class RetryingTests: XCTestCase { + func testWhenBlockSucceedsRightAway() throws { + let randomValue: Int = .random(in: 0..<100) + + // When + let mock = OperationMock([.success(randomValue)]) + let result = try retry(times: 3, delay: 0.001, block: mock.operation) + + // Then + XCTAssertEqual(mock.callsReceived, 1) + XCTAssertEqual(result, randomValue) + } + + func testWhenBlockSucceedsInFirstRetry() throws { + let randomValue: Int = .random(in: 0..<100) + + // When + let mock = OperationMock([.failure(ErrorMock()), .success(randomValue)]) + let result = try retry(times: 3, delay: 0.001, block: mock.operation) + + // Then + XCTAssertEqual(mock.callsReceived, 2) + XCTAssertEqual(result, randomValue) + } + + func testWhenBlockSucceedsInLastRetry() throws { + let randomValue: Int = .random(in: 0..<100) + + // When + let mock = OperationMock([.failure(ErrorMock()), .failure(ErrorMock()), .success(randomValue)]) + let result = try retry(times: 3, delay: 0.001, block: mock.operation) + + // Then + XCTAssertEqual(mock.callsReceived, 3) + XCTAssertEqual(result, randomValue) + } + + func testWhenBlockDoesNotSucceedInRetry() throws { + let anyError = ErrorMock(.mockAny()) + let lastError = ErrorMock(.mockRandom()) + + // When + let mock = OperationMock([.failure(anyError), .failure(anyError), .failure(lastError)]) + XCTAssertThrowsError(try retry(times: 3, delay: 0.001, block: mock.operation)) { error in + XCTAssertEqual((error as? ErrorMock)?.description, lastError.description) + } + + // Then + XCTAssertEqual(mock.callsReceived, 3) + } +} diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift new file mode 100644 index 0000000000..9f45e3b5b7 --- /dev/null +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift @@ -0,0 +1,320 @@ +/* + * 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 +#if canImport(CoreTelephony) +import CoreTelephony +#endif + +import DatadogInternal +import TestUtilities + +@testable import DatadogLogs +@testable import DatadogRUM +@testable import DatadogCrashReporting +@testable import DatadogCore + +/// This suite tests if `CrashContextProvider` gets updated by different SDK components, each updating +/// separate part of the `CrashContext` information. +class CrashContextProviderTests: XCTestCase { + private let provider = CrashContextCoreProvider() + + // MARK: - Receiving SDK Context + + func testWhenInitialSDKContextIsReceived_itNotifiesCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + } + + func testWhenNextSDKContextIsReceived_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) // receive next + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + } + + // MARK: - Receiving RUM View + + func testWhenRUMViewIsReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumView: RUMViewEvent = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMViewEvent, rumView, "Last RUM view must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMView_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumView: RUMViewEvent = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMViewEvent, rumView, "Last RUM view must be available even after next SDK context update") + } + + // MARK: - Receiving RUM View Reset + + func testWhenRUMViewResetIsReceivedAfterRUMView_thenItNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumView: RUMViewEvent = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + XCTAssertNil(crashContext.lastRUMViewEvent, "Last RUM view must reset") + } + + func testWhenSDKContextIsReceivedAfterRUMViewReset_thenItNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumView: RUMViewEvent = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: rumView), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + XCTAssertNil(crashContext.lastRUMViewEvent, "Last RUM view must reset even after next SDK context update") + } + + // MARK: - Receiving RUM Session State + + func testWhenRUMSessionStateIsReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumSessionState: RUMSessionState = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: rumSessionState), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMSessionState, rumSessionState, "Last RUM session state must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMSessionState_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumSessionState: RUMSessionState = .mockRandom() + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: rumSessionState), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMSessionState, rumSessionState, "Last RUM session state must be available even after next SDK context update") + } + + // MARK: - Receiving Global RUM Attributes + + func testWhenRUMAttributesAreReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let rumAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.attributes, value: rumAttributes), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastRUMAttributes, rumAttributes, "Last RUM attributes must be available") + } + + func testWhenSDKContextIsReceivedAfterRUMAttributes_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let rumAttributes = GlobalRUMAttributes(attributes: mockRandomAttributes()) + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: RUMBaggageKeys.attributes, value: rumAttributes), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastRUMAttributes, rumAttributes, "Last RUM attributes must be available even after next SDK context update") + } + + // MARK: - Receiving Global Log Attributes + + func testWhenLogAttributesAreReceivedAfterSDKContext_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let sdkContext: DatadogContext = .mockRandom() + let logAttributes = AnyCodable(mockRandomAttributes()) + + // When + XCTAssertTrue(provider.receive(message: .context(sdkContext), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: GlobalLogAttributes.key, value: logAttributes), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: sdkContext) + DDAssertJSONEqual(crashContext.lastLogAttributes, logAttributes, "Last Log attributes must be available") + } + + func testWhenSDKContextIsReceivedAfterLogAttributes_itNotifiesNewCrashContext() throws { + var latestCrashContext: CrashContext? = nil + provider.onCrashContextChange = { latestCrashContext = $0 } + + // Given + let logAttributes = AnyCodable(mockRandomAttributes()) + let nextSDKContext: DatadogContext = .mockRandom() + + // When + XCTAssertTrue(provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore())) // receive initial SDK context + XCTAssertTrue(provider.receive(message: .baggage(key: GlobalLogAttributes.key, value: logAttributes), from: NOPDatadogCore())) + XCTAssertTrue(provider.receive(message: .context(nextSDKContext), from: NOPDatadogCore())) + + // Then + provider.flush() + let crashContext = try XCTUnwrap(latestCrashContext) + XCTAssertEqual(crashContext, provider.currentCrashContext) + DDAssert(crashContext: crashContext, includes: nextSDKContext) + DDAssertJSONEqual(crashContext.lastLogAttributes, logAttributes, "Last Log attributes must be available even after next SDK context update") + } + + // MARK: - Thread safety + + func testWhenContextIsWrittenAndReadFromDifferentThreads_itRunsAllOperationsSafely() { + let provider = CrashContextCoreProvider() + let viewEvent: RUMViewEvent = .mockRandom() + let sessionState: RUMSessionState = .mockRandom() + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { _ = provider.currentCrashContext }, + { _ = provider.receive(message: .context(.mockRandom()), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.viewReset, value: true), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.viewEvent, value: viewEvent), from: NOPDatadogCore()) }, + { _ = provider.receive(message: .baggage(key: RUMBaggageKeys.sessionState, value: sessionState), from: NOPDatadogCore()) }, + ], + iterations: 50 + ) + // swiftlint:enable opening_brace + + provider.flush() + } + + // MARK: - Helpers + + private func DDAssert(crashContext: CrashContext, includes sdkContext: DatadogContext, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(crashContext.appLaunchDate, sdkContext.launchTime?.launchDate, file: file, line: line) + XCTAssertEqual(crashContext.serverTimeOffset, sdkContext.serverTimeOffset, file: file, line: line) + XCTAssertEqual(crashContext.service, sdkContext.service, file: file, line: line) + XCTAssertEqual(crashContext.env, sdkContext.env, file: file, line: line) + XCTAssertEqual(crashContext.version, sdkContext.version, file: file, line: line) + XCTAssertEqual(crashContext.buildNumber, sdkContext.buildNumber, file: file, line: line) + XCTAssertEqual(crashContext.device, sdkContext.device, file: file, line: line) + XCTAssertEqual(crashContext.sdkVersion, sdkContext.sdkVersion, file: file, line: line) + XCTAssertEqual(crashContext.source, sdkContext.source, file: file, line: line) + XCTAssertEqual(crashContext.trackingConsent, sdkContext.trackingConsent, file: file, line: line) + DDAssertReflectionEqual(crashContext.userInfo, sdkContext.userInfo, file: file, line: line) + XCTAssertEqual(crashContext.networkConnectionInfo, sdkContext.networkConnectionInfo, file: file, line: line) + XCTAssertEqual(crashContext.carrierInfo, sdkContext.carrierInfo, file: file, line: line) + XCTAssertEqual(crashContext.lastIsAppInForeground, sdkContext.applicationStateHistory.currentSnapshot.state.isRunningInForeground, file: file, line: line) + } +} diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift new file mode 100644 index 0000000000..40e8fd2443 --- /dev/null +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift @@ -0,0 +1,183 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogCore +@testable import DatadogCrashReporting + +class CrashContextTests: XCTestCase { + /// This must be the exact encoder used to encode `CrashContext` in production code. + private let encoder = CrashReportingFeature.crashContextEncoder + /// This must be the exact decoder used to decode `CrashContext` in production code. + private let decoder = CrashReportingFeature.crashContextDecoder + + func testGivenContextWithTrackingConsentSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomConsent: TrackingConsent = .mockRandom() + + // Given + let context: CrashContext = .mockWith(trackingConsent: randomConsent) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.trackingConsent, randomConsent) + } + + func testGivenContextWithLastRUMViewEventSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomRUMViewEvent = AnyCodable(mockRandomAttributes()) + + // Given + let context: CrashContext = .mockWith(lastRUMViewEvent: randomRUMViewEvent) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastRUMViewEvent, + randomRUMViewEvent + ) + } + + func testGivenContextWithLastRUMSessionStateSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomRUMSessionState = Bool.random() ? + AnyCodable(mockRandomAttributes()) : nil + + // Given + let context: CrashContext = .mockWith(lastRUMSessionState: randomRUMSessionState) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastRUMSessionState, + randomRUMSessionState + ) + } + + func testGivenContextWithLastRUMAttributesSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomRUMAttributes = Bool.random() ? GlobalRUMAttributes(attributes: mockRandomAttributes()) : nil + + // Given + let context: CrashContext = .mockWith(lastRUMAttributes: randomRUMAttributes) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastRUMAttributes, + randomRUMAttributes + ) + } + + func testGivenContextWithLastLogttributesSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomLogAttributes = Bool.random() ? AnyCodable(mockRandomAttributes()) : nil + + // Given + let context: CrashContext = .mockWith(lastLogAttributes: randomLogAttributes) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + DDAssertJSONEqual( + deserializedContext.lastLogAttributes, + randomLogAttributes + ) + } + + func testGivenContextWithUserInfoSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomUserInfo: UserInfo = .mockRandom() + + // Given + let context: CrashContext = .mockWith(userInfo: randomUserInfo) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.userInfo?.id, randomUserInfo.id) + XCTAssertEqual(deserializedContext.userInfo?.name, randomUserInfo.name) + XCTAssertEqual(deserializedContext.userInfo?.email, randomUserInfo.email) + + DDAssertJSONEqual( + deserializedContext.userInfo!.extraInfo.mapValues { AnyEncodable($0) }, + randomUserInfo.extraInfo.mapValues { AnyEncodable($0) } + ) + } + + func testGivenContextWithNetworkConnectionInfoSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomNetworkConnectionInfo: NetworkConnectionInfo = .mockRandom() + + // Given + let context: CrashContext = .mockWith(networkConnectionInfo: randomNetworkConnectionInfo) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.networkConnectionInfo, randomNetworkConnectionInfo) + } + + func testGivenContextWithCarrierInfoSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomCarrierInfo: CarrierInfo = .mockRandom() + + // Given + let context: CrashContext = .mockWith(carrierInfo: randomCarrierInfo) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.carrierInfo, randomCarrierInfo) + } + + func testGivenContextWithIsAppInForeground_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomIsAppInForeground: Bool = .mockRandom() + + // Given + let context: CrashContext = .mockWith(lastIsAppInForeground: randomIsAppInForeground) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.lastIsAppInForeground, randomIsAppInForeground) + } + + func testGivenContextWithAppLaunchDate_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomDate: Date = .mockRandom() + + // Given + let context: CrashContext = .mockWith(appLaunchDate: randomDate) + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual( + deserializedContext.appLaunchDate!.timeIntervalSince1970, + randomDate.timeIntervalSince1970, + accuracy: 0.001 // assert with ms precision as we encode dates as ISO8601 string which is lossfull + ) + } +} diff --git a/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift b/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift new file mode 100644 index 0000000000..d79dfa4c3a --- /dev/null +++ b/DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift @@ -0,0 +1,344 @@ +/* + * 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 DatadogLogs + +@testable import DatadogCore +@testable import DatadogCrashReporting + +class CrashReporterTests: XCTestCase { + // MARK: - Sending Crash Report + + func testWhenPendingCrashReportIsFound_itIsSentAndPurged() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report") + let crashContext: CrashContext = .mockRandom() + let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = crashReport + plugin.injectedContextData = crashContext.data + + // When + let sender = CrashReportSenderMock() + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: sender, + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // Then + sender.didSendCrashReport = { expectation.fulfill() } + feature.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + DDAssertReflectionEqual(sender.sentCrashReport, crashReport, "It should send the crash report retrieved from the `plugin`") + let sentCrashContext = try XCTUnwrap(sender.sentCrashContext, "It should send the crash context") + DDAssertDictionariesEqual( + try sentCrashContext.data.toJSONObject(), + try crashContext.data.toJSONObject(), + "It should send the crash context retrieved from the `plugin`" + ) + + XCTAssertTrue(plugin.hasPurgedCrashReport == true, "It should ask to purge the crash report") + } + + func testWhenPendingCrashReportIsFound_itIsSentToRumFeature() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to RUM feature") + let crashContext: CrashContext = .mockRandom() + let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let rumCrashReceiver = RUMCrashReceiverMock() + + let core = PassthroughCoreMock(messageReceiver: rumCrashReceiver) + + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = crashReport + plugin.injectedContextData = crashContext.data + + // When + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + //Then + plugin.didReadPendingCrashReport = { expectation.fulfill() } + feature.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + + XCTAssertNotNil(rumCrashReceiver.receivedBaggage, "RUM baggage must not be empty") + } + + func testWhenPendingCrashReportIsFound_itIsSentToLogsFeature() throws { + let expectation = self.expectation(description: "`CrashReportSender` sends the crash report to Logs feature") + let crashContext: CrashContext = .mockRandom() + let crashReport: DDCrashReport = .mockRandomWith(context: crashContext) + let logsCrashReceiver = LogsCrashReceiverMock() + + let core = PassthroughCoreMock(messageReceiver: logsCrashReceiver) + + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = crashReport + plugin.injectedContextData = crashContext.data + + // When + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + //Then + plugin.didReadPendingCrashReport = { expectation.fulfill() } + feature.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + + XCTAssertNotNil(logsCrashReceiver.receivedBaggage, "Logs baggage must not be empty") + } + + func testWhenPendingCrashReportIsNotFound_itDoesNothing() { + let expectation = self.expectation(description: "`plugin` checks the crash report") + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = nil + plugin.injectedContextData = nil + + // When + let sender = CrashReportSenderMock() + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: sender, + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // Then + plugin.didReadPendingCrashReport = { expectation.fulfill() } + feature.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertNil(sender.sentCrashReport, "It should not send the crash report") + XCTAssertNil(sender.sentCrashContext, "It should not send the crash context") + XCTAssertTrue(plugin.hasPurgedCrashReport == false, "It should not purge the crash report") + } + + func testWhenPendingCrashReportIsFoundButItHasUnavailableCrashContext_itPurgesTheCrashReportWithNoSending() { + let expectation = self.expectation(description: "`CrashReportSender` does not send the crash report") + expectation.isInverted = true + let plugin = CrashReportingPluginMock() + + // Given + plugin.pendingCrashReport = .mockWith(context: nil) + plugin.injectedContextData = nil + + // When + let sender = CrashReportSenderMock() + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: sender, + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // Then + sender.didSendCrashReport = { expectation.fulfill() } + feature.sendCrashReportIfFound() + + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertTrue( + plugin.hasPurgedCrashReport == true, + "It should ask to purge the crash report as the crash context is unavailable" + ) + } + + // MARK: - Crash Context Injection + + func testWhenInitialized_itInjectsInitialCrashContextToThePlugin() throws { + let expectation = self.expectation(description: "`plugin` received initial crash context") + let plugin = CrashReportingPluginMock() + plugin.didInjectContext = { expectation.fulfill() } + + // When + let initialCrashContext: CrashContext = .mockRandom() + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(initialCrashContext: initialCrashContext), + sender: CrashReportSenderMock(), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + try withExtendedLifetime(feature) { + // Then + waitForExpectations(timeout: 0.5, handler: nil) + DDAssertDictionariesEqual( + try plugin.injectedContextData!.toJSONObject(), + try initialCrashContext.data.toJSONObject() + ) + } + } + + func testWhenCrashContextChanges_itInjectsNewCrashContextToThePlugin() throws { + let expectation = self.expectation(description: "`plugin` received initial and updated crash contexts") + expectation.expectedFulfillmentCount = 2 + let plugin = CrashReportingPluginMock() + plugin.didInjectContext = { expectation.fulfill() } + + let crashContextProvider = CrashContextProviderMock(initialCrashContext: .mockRandom()) + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: crashContextProvider, + sender: CrashReportSenderMock(), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + try withExtendedLifetime(feature) { + // When + let updatedCrashContext: CrashContext = .mockRandom() + crashContextProvider.onCrashContextChange(updatedCrashContext) + + // Then + waitForExpectations(timeout: 2, handler: nil) + DDAssertDictionariesEqual( + try plugin.injectedContextData!.toJSONObject(), + try updatedCrashContext.data.toJSONObject() + ) + } + } + + func testGivenAnyCrashWithUnauthorizedTrackingConsent_whenSending_itIsDropped() throws { + let expectation = self.expectation(description: "`plugin` checks the crash report") + // Given + let core = PassthroughCoreMock() + let lastRUMViewEvent = Bool.random() ? + AnyCodable(mockRandomAttributes()) : nil + + let crashReport: DDCrashReport = .mockWith( + date: .mockDecember15th2019At10AMUTC(), + context: CrashContext.mockWith( + trackingConsent: [.pending, .notGranted].randomElement()!, + lastRUMViewEvent: lastRUMViewEvent // no matter if in RUM session or not + ).data + ) + + let plugin = CrashReportingPluginMock() + plugin.pendingCrashReport = crashReport + plugin.didReadPendingCrashReport = { expectation.fulfill() } + + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // When + feature.sendCrashReportIfFound() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertEqual(core.events.count, 0, "Crash must not be send as it doesn't have `.granted` consent") + } + + // MARK: - Thread safety + + 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 + + // State mutated by the mock plugin implementation - `DatadogCrashReporter` ensures its thread safety + var mutableState: Bool = .random() + + let plugin = CrashReportingPluginMock() + plugin.didInjectContext = { + mutableState.toggle() + expectation.fulfill() + } + + let crashContextProvider = CrashContextProviderMock(initialCrashContext: .mockRandom()) + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: crashContextProvider, + sender: CrashReportSenderMock(), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { crashContextProvider.onCrashContextChange(.mockRandom()) }, + { crashContextProvider.onCrashContextChange(.mockRandom()) } + ], + iterations: 50 // each closure is called 50 times + ) + // swiftlint:enable opening_brace + + feature.flush() + waitForExpectations(timeout: 0) + } + + // MARK: - Usage + + func testGivenNoRegisteredCrashReportReceiver_whenPendingCrashReportIsFound_itPrintsWarning() { + let expectation = self.expectation(description: "`plugin` checks the crash report") + + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let core = PassthroughCoreMock() + let plugin = CrashReportingPluginMock() + plugin.pendingCrashReport = .mockWith( + context: CrashContext.mockAny().data + ) + + plugin.didReadPendingCrashReport = { expectation.fulfill() } + + // When + let feature = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: CrashContextProviderMock(), + sender: MessageBusSender(core: core), + messageReceiver: NOPFeatureMessageReceiver(), + telemetry: NOPTelemetry() + ) + + // When + feature.sendCrashReportIfFound() + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + let logs = dd.logger.warnLogs + + XCTAssert(logs.contains(where: { $0.message == """ + In order to use Crash Reporting, RUM or Logging feature must be enabled. + Make sure `RUM` or `Logs` are enabled when initializing Datadog SDK. + """ + })) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift b/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift new file mode 100644 index 0000000000..7021aae826 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogConfigurationTests.swift @@ -0,0 +1,429 @@ +/* + * 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 + +@testable import DatadogCore + +class DatadogConfigurationTests: XCTestCase { + private var printFunction: PrintFunctionMock! // swiftlint:disable:this implicitly_unwrapped_optional + private var defaultConfig = Datadog.Configuration(clientToken: "abc-123", env: "tests") + + override func setUp() { + super.setUp() + + XCTAssertFalse(Datadog.isInitialized()) + printFunction = PrintFunctionMock() + consolePrint = printFunction.print + } + + override func tearDown() { + consolePrint = { message, _ in print(message) } + printFunction = nil + XCTAssertFalse(Datadog.isInitialized()) + super.tearDown() + } + + // MARK: - Initializing with different configurations + + func testDefaultConfiguration() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + bundleIdentifier: "test", + CFBundleShortVersionString: "1.0.0", + CFBundleExecutable: "Test" + ) + + XCTAssertEqual(configuration.batchSize, .medium) + XCTAssertEqual(configuration.uploadFrequency, .average) + XCTAssertEqual(configuration.additionalConfiguration.count, 0) + XCTAssertNil(configuration.encryption) + XCTAssertTrue(configuration.serverDateProvider is DatadogNTPDateProvider) + + Datadog.initialize( + with: configuration, + trackingConsent: .granted + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let urlSessionClient = try XCTUnwrap(core.httpClient as? URLSessionClient) + XCTAssertTrue(core.dateProvider is SystemDateProvider) + XCTAssertNil(urlSessionClient.session.configuration.connectionProxyDictionary) + XCTAssertNil(core.encryption) + + let context = core.contextProvider.read() + XCTAssertEqual(context.clientToken, "abc-123") + XCTAssertEqual(context.env, "tests") + XCTAssertEqual(context.site, .us1) + XCTAssertEqual(context.service, "test") + XCTAssertEqual(context.version, "1.0.0") + XCTAssertEqual(context.sdkVersion, __sdkVersion) + XCTAssertEqual(context.applicationName, "Test") + XCTAssertNil(context.variant) + XCTAssertEqual(context.source, "ios") + XCTAssertEqual(context.applicationBundleIdentifier, "test") + XCTAssertEqual(context.trackingConsent, .granted) + } + + func testAdvancedConfiguration() throws { + var configuration = defaultConfig + + configuration.service = "service-name" + configuration.site = .eu1 + configuration.batchSize = .small + configuration.uploadFrequency = .frequent + configuration.batchProcessingLevel = .high + configuration.proxyConfiguration = [ + kCFNetworkProxiesHTTPEnable: true, + kCFNetworkProxiesHTTPPort: 123, + kCFNetworkProxiesHTTPProxy: "www.example.com", + kCFProxyUsernameKey: "proxyuser", + kCFProxyPasswordKey: "proxypass", + ] + configuration.bundle = .mockWith( + bundleIdentifier: "test", + CFBundleShortVersionString: "1.0.0", + CFBundleExecutable: "Test" + ) + configuration.encryption = DataEncryptionMock() + configuration.serverDateProvider = ServerDateProviderMock() + configuration._internal_mutation { + $0.additionalConfiguration = [ + CrossPlatformAttributes.ddsource: "cp-source", + CrossPlatformAttributes.variant: "cp-variant", + CrossPlatformAttributes.sdkVersion: "cp-version" + ] + } + + XCTAssertEqual(configuration.batchSize, .small) + XCTAssertEqual(configuration.uploadFrequency, .frequent) + XCTAssertEqual(configuration.batchProcessingLevel, .high) + XCTAssertTrue(configuration.encryption is DataEncryptionMock) + XCTAssertTrue(configuration.serverDateProvider is ServerDateProviderMock) + + Datadog.initialize( + with: configuration, + trackingConsent: .pending + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + XCTAssertTrue(core.dateProvider is SystemDateProvider) + XCTAssertTrue(core.encryption is DataEncryptionMock) + + let urlSessionClient = try XCTUnwrap(core.httpClient as? URLSessionClient) + let connectionProxyDictionary = try XCTUnwrap(urlSessionClient.session.configuration.connectionProxyDictionary) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPEnable] as? Bool, true) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPPort] as? Int, 123) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPProxy] as? String, "www.example.com") + XCTAssertEqual(connectionProxyDictionary[kCFProxyUsernameKey] as? String, "proxyuser") + XCTAssertEqual(connectionProxyDictionary[kCFProxyPasswordKey] as? String, "proxypass") + + let context = core.contextProvider.read() + XCTAssertEqual(context.clientToken, "abc-123") + XCTAssertEqual(context.env, "tests") + XCTAssertEqual(context.site, .eu1) + XCTAssertEqual(context.service, "service-name") + XCTAssertEqual(context.version, "1.0.0") + XCTAssertEqual(context.sdkVersion, "cp-version") + XCTAssertEqual(context.applicationName, "Test") + XCTAssertEqual(context.variant, "cp-variant") + XCTAssertEqual(context.source, "cp-source") + XCTAssertEqual(context.applicationBundleIdentifier, "test") + XCTAssertEqual(context.trackingConsent, .pending) + } + + func testGivenDefaultConfiguration_itCanBeInitialized() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + XCTAssertTrue(Datadog.isInitialized()) + Datadog.flushAndDeinitialize() + } + + func testGivenInvalidConfiguration_itPrintsError() { + let invalidConfiguration = Datadog.Configuration(clientToken: "", env: "tests") + + Datadog.initialize( + with: invalidConfiguration, + trackingConsent: .mockRandom() + ) + + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: `clientToken` cannot be empty." + ) + XCTAssertFalse(Datadog.isInitialized()) + } + + func testGivenValidConfiguration_whenInitializedMoreThanOnce_itPrintsError() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: The 'main' instance of SDK is already initialized." + ) + + Datadog.flushAndDeinitialize() + } + + func testGivenNoExecutable_itUsesBundleTypeAsApplicationName() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + CFBundleExecutable: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationName, "iOSApp") + } + + func testGivenNoExecutable_andWidgetExecutable_itUsesBundleTypeAsApplicationName() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + bundlePath: "widget.appex", + CFBundleExecutable: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationName, "iOSAppExtension") + } + + func testGivenNoBundleVersion_itUsesShortVersionString() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + CFBundleVersion: nil, + CFBundleShortVersionString: "1.2.3" + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.version, "1.2.3") + } + + func testGivenNoBundleShortVersion_itUsesDefaultValue() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + CFBundleVersion: nil, + CFBundleShortVersionString: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.version, "0.0.0") + XCTAssertEqual(context.buildNumber, "0") + } + + func testGivenNoBundleVersion_itUsesDefaultValue() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + CFBundleVersion: "FFFFF", + CFBundleShortVersionString: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.buildNumber, "FFFFF") + } + + func testGivenNoBundleIdentifier_itUsesDefaultValues() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + bundleIdentifier: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationBundleIdentifier, "unknown") + XCTAssertEqual(context.service, "ios") + } + + func testGivenNoBundleIdentifier_itUsesUnkown() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + bundleIdentifier: nil + ) + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationBundleIdentifier, "unknown") + } + + func testiOSAppBundleType() throws { + var configuration = defaultConfig + configuration.bundle = .mockWith(bundlePath: "bundle.path.app") + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationBundleType, .iOSApp) + } + + func testiOSAppExtensionBundleType() throws { + var configuration = defaultConfig + configuration.bundle = .mockWith(bundlePath: "bundle.path.appex") + + Datadog.initialize( + with: configuration, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.applicationBundleType, .iOSAppExtension) + } + + func testEnvironment() throws { + func verify(validEnv env: String) throws { + Datadog.initialize( + with: Datadog.Configuration(clientToken: "abc-123", env: env), + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + XCTAssertNil(printFunction.printedMessage) + } + + func verify(invalidEnv env: String) { + Datadog.initialize( + with: Datadog.Configuration(clientToken: "abc-123", env: env), + trackingConsent: .mockRandom() + ) + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: `env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)" + ) + } + + try verify(validEnv: "staging_1") + try verify(validEnv: "production") + try verify(validEnv: "production:some") + try verify(validEnv: "pro/d-uct.ion_") + + verify(invalidEnv: "") + verify(invalidEnv: "*^@!&#") + verify(invalidEnv: "abc def") + verify(invalidEnv: "*^@!&#") + verify(invalidEnv: "*^@!&#\nsome_env") + verify(invalidEnv: String(repeating: "a", count: 197)) + } + + func testApplicationVersionOverride() throws { + var configuration = defaultConfig + configuration.additionalConfiguration[CrossPlatformAttributes.version] = "5.23.2" + + Datadog.initialize(with: configuration, trackingConsent: .mockRandom()) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + + XCTAssertEqual(context.version, "5.23.2") + } + + func testGivenBuildId_itSetsContext() throws { + // Given + let buildId: String = .mockRandom(length: 32) + var configuration = defaultConfig + configuration.additionalConfiguration[CrossPlatformAttributes.buildId] = buildId + + // When + Datadog.initialize(with: configuration, trackingConsent: .mockRandom()) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + + // Then + XCTAssertEqual(context.buildId, buildId) + } + + func testGivenNativeSourceType_itSetsInContext() throws { + // Given + let nativeSourceType: String = .mockRandom() + var configuration = defaultConfig + configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] = nativeSourceType + + // When + Datadog.initialize(with: configuration, trackingConsent: .mockRandom()) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + + // Then + XCTAssertEqual(context.nativeSourceOverride, nativeSourceType) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift new file mode 100644 index 0000000000..ffa82c98cf --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift @@ -0,0 +1,87 @@ +/* + * 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 +import TestUtilities +@testable import DatadogCore + +class ApplicationStatePublisherTests: XCTestCase { + private let notificationCenter = NotificationCenter() + + private let supportedNotifications = [ + (name: UIApplication.didBecomeActiveNotification, expectedState: AppState.active), + (name: UIApplication.willResignActiveNotification, expectedState: AppState.inactive), + (name: UIApplication.didEnterBackgroundNotification, expectedState: AppState.background), + (name: UIApplication.willEnterForegroundNotification, expectedState: AppState.inactive), + ] + + // MARK: - Handling UIApplication Notifications + + func testWhenReceivingAppLifecycleNotification_itRecordsItsState() { + let expectation = expectation(description: "app state publisher publishes value") + + // Given + let publisher = ApplicationStatePublisher( + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: SystemDateProvider() + ) + + // When + let notification = supportedNotifications.randomElement()! + + publisher.publish { state in + // Then + XCTAssertEqual( + state.currentSnapshot.state, + notification.expectedState, + "It must record \(notification.expectedState) after receiving '\(notification.name)'" + ) + expectation.fulfill() + } + + notificationCenter.post(name: notification.name, object: nil) + + waitForExpectations(timeout: 1) + } + + // MARK: - Recording History + + func testWhenReceivingStateChangeNotifications_itRecordsHistoryOfAppStates() { + let expectation = expectation(description: "app state publisher publishes values") + expectation.expectedFulfillmentCount = 100 + + // Given + let publisher = ApplicationStatePublisher( + appStateProvider: AppStateProviderMock(state: .mockRandom()), + notificationCenter: notificationCenter, + dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0) + ) + + var receivedHistoryStates: [AppState?] = [] + let expectedNotifications = (0..(initialValue: 0) + let networkConnectionInfoPublisher = ContextValuePublisherMock() + let carrierInfoPublisher = ContextValuePublisherMock() + + let provider = DatadogContextProvider(context: context) + provider.subscribe(\.serverTimeOffset, to: serverOffsetPublisher) + provider.subscribe(\.networkConnectionInfo, to: networkConnectionInfoPublisher) + provider.subscribe(\.carrierInfo, to: carrierInfoPublisher) + + // When + let serverTimeOffset: TimeInterval = .mockRandomInThePast() + serverOffsetPublisher.value = serverTimeOffset + + let networkConnectionInfo: NetworkConnectionInfo = .mockRandom() + networkConnectionInfoPublisher.value = networkConnectionInfo + + let carrierInfo: CarrierInfo = .mockRandom() + carrierInfoPublisher.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 + + // Given + let serverOffsetPublisher = ContextValuePublisherMock(initialValue: 0) + + let provider = DatadogContextProvider(context: context) + provider.subscribe(\.serverTimeOffset, to: serverOffsetPublisher) + + provider.publish { _ in + expectation.fulfill() + } + + // When + (0..(initialValue: 0) + let networkConnectionInfoPublisher = ContextValuePublisherMock() + let carrierInfoPublisher = ContextValuePublisherMock() + + let provider = DatadogContextProvider(context: context) + + provider.subscribe(\.serverTimeOffset, to: serverOffsetPublisher) + provider.subscribe(\.networkConnectionInfo, to: networkConnectionInfoPublisher) + provider.subscribe(\.carrierInfo, to: carrierInfoPublisher) + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { serverOffsetPublisher.value = .mockRandom() }, + { networkConnectionInfoPublisher.value = .mockRandom() }, + { carrierInfoPublisher.value = .mockRandom() }, + { provider.read { _ in } }, + { provider.write { $0 = .mockAny() } } + ], + iterations: 1_000 + ) + // swiftlint:enable opening_brace + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/FeatureContextTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/FeatureContextTests.swift new file mode 100644 index 0000000000..608e313b35 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/FeatureContextTests.swift @@ -0,0 +1,42 @@ +/* + * 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 +import TestUtilities +@testable import DatadogCore + +class FeatureContextTests: XCTestCase { + func testV2FeatureContextSharing() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .mockAny(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + + defer { temporaryCoreDirectory.delete() } + + // When + let attributes = ["key": "value"] + core.set(baggage: attributes, forKey: "test") + + // Then + let context = core.contextProvider.read() + let testBaggage = try XCTUnwrap(context.baggages["test"]) + try DDAssertDictionariesEqual( + testBaggage.decode(type: [String: String].self), + attributes + ) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/LaunchTimePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/LaunchTimePublisherTests.swift new file mode 100644 index 0000000000..076fd84d0d --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/LaunchTimePublisherTests.swift @@ -0,0 +1,66 @@ +/* + * 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 DatadogCore + +class LaunchTimePublisherTests: XCTestCase { + override func tearDown() { + super.tearDown() + setenv("ActivePrewarm", "", 1) + } + + func testGivenStartedApplication_itHasLaunchDate() throws { + // Given + let publisher = LaunchTimePublisher() + + // When + let launchTime = publisher.initialValue + + // Then + XCTAssertNotNil(launchTime?.launchDate) + } + + func testThreadSafety() { + let handler = __dd_private_AppLaunchHandler.shared + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { _ = handler.launchTime }, + { _ = handler.launchDate }, + { _ = handler.isActivePrewarm }, + { handler.setApplicationDidBecomeActiveCallback { _ in } } + ], + iterations: 1_000 + ) + // swiftlint:enable opening_brace + } + + func testIsActivePrewarm_returnsTrue() { + // Given + setenv("ActivePrewarm", "1", 1) + NSClassFromString("__dd_private_AppLaunchHandler")?.load() + + // When + let publisher = LaunchTimePublisher() + + // Then + XCTAssertTrue(publisher.initialValue?.isActivePrewarm ?? false) + } + + func testIsActivePrewarm_returnsFalse() { + // Given + NSClassFromString("__dd_private_AppLaunchHandler")?.load() + + // When + let publisher = LaunchTimePublisher() + + // Then + XCTAssertFalse(publisher.initialValue?.isActivePrewarm ?? true) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift new file mode 100644 index 0000000000..7e79241b5c --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift @@ -0,0 +1,40 @@ +/* + * 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 DatadogCore + +class LowPowerModePublisherTests: XCTestCase { + private let notificationCenter = NotificationCenter() + + func testGivenInitialLowPowerModeSettingValue_whenSettingChanges_itUpdatesIsLowPowerModeEnabledValue() { + let expectation = self.expectation(description: "Publish `isLowPowerModeEnabled`") + + // Given + let isLowPowerModeEnabled: Bool = .random() + let publisher = LowPowerModePublisher( + notificationCenter: notificationCenter, + processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled) + ) + + XCTAssertEqual(publisher.initialValue, isLowPowerModeEnabled) + + // When + publisher.publish { + // Then + XCTAssertNotEqual($0, isLowPowerModeEnabled) + expectation.fulfill() + } + + notificationCenter.post( + name: .NSProcessInfoPowerStateDidChange, + object: ProcessInfoMock(isLowPowerModeEnabled: !isLowPowerModeEnabled) + ) + + waitForExpectations(timeout: 0.5, handler: nil) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/NetworkConnectionInfoPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/NetworkConnectionInfoPublisherTests.swift new file mode 100644 index 0000000000..3799e8d404 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/NetworkConnectionInfoPublisherTests.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 XCTest +import Network +import SystemConfiguration +import DatadogInternal +@testable import DatadogCore + +class NetworkConnectionInfoPublisherTests: XCTestCase { + func testNWPathMonitorPublishValue() { + let expectation = expectation(description: "NWPathMonitorPublisher publish value") + let publisher = NWPathMonitorPublisher() + publisher.publish { _ in expectation.fulfill() } + waitForExpectations(timeout: 1, handler: nil) + } + + func testNWPathMonitorHandling() { + let monitor = NWPathMonitor() + let publisher = NWPathMonitorPublisher(monitor: monitor) + publisher.publish { _ in } + XCTAssertNotNil(monitor.pathUpdateHandler, "`NWPathMonitor` has a handler") + XCTAssertNotNil(monitor.queue, "`NWPathMonitor` is started with synchronization queue") + } +} + +class NetworkConnectionInfoConversionTests: XCTestCase { + typealias Reachability = NetworkConnectionInfo.Reachability + typealias Interface = NetworkConnectionInfo.Interface + + func testNWPathStatus() { + XCTAssertEqual(Reachability(.satisfied), .yes) + XCTAssertEqual(Reachability(.unsatisfied), .no) + XCTAssertEqual(Reachability(.requiresConnection), .maybe) + } + + func testNWInterface() { + XCTAssertEqual(Interface(.wifi), .wifi) + XCTAssertEqual(Interface(.wiredEthernet), .wiredEthernet) + XCTAssertEqual(Interface(.loopback), .loopback) + XCTAssertEqual(Interface(.cellular), .cellular) + XCTAssertEqual(Interface(.other), .other) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ServerOffsetPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ServerOffsetPublisherTests.swift new file mode 100644 index 0000000000..e18a75254e --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ServerOffsetPublisherTests.swift @@ -0,0 +1,118 @@ +/* + * 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 +@testable import DatadogCore + +class ServerOffsetPublisherTests: XCTestCase { + func testPickRandomDatadogNTPServers() throws { + let kronos = KronosClockMock() + let provider = DatadogNTPDateProvider(kronos: kronos) + let publisher = ServerOffsetPublisher(provider: provider) + + var pools: Set = [] + + try (0..<100).forEach { _ in + publisher.publish { _ in } + let pool = try XCTUnwrap(kronos.currentPool) + XCTAssertTrue(pool.hasSuffix(".datadog.pool.ntp.org")) + pools.insert(pool) + } + + XCTAssertEqual(pools, Set(DatadogNTPServers), "Each time Datadog NTP server should be picked randomly.") + } + + func testWhenSyncSucceedsOnce_itPublishesOffset() throws { + let expectation = expectation(description: "kronos publisher publishes offset") + + // Given + let kronos = KronosClockMock() + let provider = DatadogNTPDateProvider(kronos: kronos) + let publisher = ServerOffsetPublisher(provider: provider) + + // When + publisher.publish { + // Then + XCTAssertEqual($0, -1) + expectation.fulfill() + } + + kronos.update(offset: -1) + + // KronosClockMock publishes in sync + waitForExpectations(timeout: 0) + } + + func testWhenSyncCompletesSuccessfully_itPublishesOffset() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let expectation = expectation(description: "kronos publisher publishes offset") + expectation.expectedFulfillmentCount = 2 + + // Given + let kronos = KronosClockMock() + let provider = DatadogNTPDateProvider(kronos: kronos) + let publisher = ServerOffsetPublisher(provider: provider) + + // When + publisher.publish { + // Then + XCTAssertEqual($0, -1) + expectation.fulfill() + } + + kronos.update(offset: -1) + kronos.complete() + + // Then + XCTAssertEqual( + dd.logger.debugLog?.message, + """ + NTP time synchronization completed. + Server time will be used for signing events (-1.0s difference with device time). + """ + ) + + // KronosClockMock publishes in sync + waitForExpectations(timeout: 0) + } + + func testWhenSyncFails_itPublishesZero() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let expectation = expectation(description: "kronos publisher publishes 0") + + // Given + let kronos = KronosClockMock() + let provider = DatadogNTPDateProvider(kronos: kronos) + let publisher = ServerOffsetPublisher(provider: provider) + + // When + publisher.publish { + // Then + XCTAssertEqual($0, .zero) + expectation.fulfill() + } + + kronos.complete() + + // Then + XCTAssertEqual( + dd.logger.errorLog?.message, + """ + NTP time synchronization failed. + Device time will be used for signing events. + """ + ) + + // KronosClockMock publishes in sync + waitForExpectations(timeout: 0) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/TrackingConsentPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/TrackingConsentPublisherTests.swift new file mode 100644 index 0000000000..adfdbbae3c --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/TrackingConsentPublisherTests.swift @@ -0,0 +1,34 @@ +/* + * 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 DatadogCore + +class TrackingConsentPublisherTests: XCTestCase { + func testInitialValue() throws { + let publisher = TrackingConsentPublisher(consent: .pending) + XCTAssertEqual(publisher.initialValue, .pending) + } + + func testPublishUserInfo() throws { + let expectation = expectation(description: "tracking consenr publisher publishes data") + + // Given + let publisher = TrackingConsentPublisher(consent: .granted) + + // When + publisher.publish { + // Then + XCTAssertEqual($0, .notGranted) + expectation.fulfill() + } + + publisher.consent = .notGranted + + // UserInfoPublisher publishes in sync + waitForExpectations(timeout: 0) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/UserInfoPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/UserInfoPublisherTests.swift new file mode 100644 index 0000000000..173a59bcf8 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/UserInfoPublisherTests.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 XCTest +import DatadogInternal +import TestUtilities +@testable import DatadogCore + +class UserInfoPublisherTests: XCTestCase { + func testEmptyInitialValue() throws { + let publisher = UserInfoPublisher() + DDAssertReflectionEqual(publisher.initialValue, .empty) + } + + func testPublishUserInfo() throws { + let expectation = expectation(description: "user info publisher publishes data") + + // Given + let publisher = UserInfoPublisher() + let userInfo: UserInfo = .mockRandom() + + // When + publisher.publish { + // Then + DDAssertReflectionEqual($0, userInfo) + expectation.fulfill() + } + + publisher.current = userInfo + + // UserInfoPublisher publishes in sync + waitForExpectations(timeout: 0) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDataStoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDataStoreTests.swift new file mode 100644 index 0000000000..94d4c63ee1 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDataStoreTests.swift @@ -0,0 +1,126 @@ +/* + * 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 +import TestUtilities +@testable import DatadogCore + +private struct FeatureAMock: DatadogRemoteFeature { + static let name: String = "feature-a" + var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() + var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() +} + +private struct FeatureBMock: DatadogRemoteFeature { + static let name: String = "feature-b" + var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() + var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() +} + +class DatadogCore_FeatureDataStoreTests: XCTestCase { + func testGivenTwoFeaturesRegistered_whenWritingToTheirDataStore_eachStoreIsUnique() throws { + let core = DatadogCore( + directory: temporaryCoreDirectory.create(), + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockAny(), + backgroundTasksEnabled: .mockAny() + ) + defer { + core.flushAndTearDown() + temporaryCoreDirectory.delete() + } + + // Given + try core.register(feature: FeatureAMock()) + try core.register(feature: FeatureBMock()) + + // When + let scopeA = core.scope(for: FeatureAMock.self) + let scopeB = core.scope(for: FeatureBMock.self) + + let commonKey = "key" + scopeA.dataStore.setValue("feature A data".utf8Data, forKey: commonKey) + scopeB.dataStore.setValue("feature B data".utf8Data, forKey: commonKey) + + // Then + var dataInA: Data? + var dataInB: Data? + scopeA.dataStore.value(forKey: commonKey) { dataInA = $0.data() } + scopeB.dataStore.value(forKey: commonKey) { dataInB = $0.data() } + + (scopeA.dataStore as? FeatureDataStore)?.flush() + (scopeB.dataStore as? FeatureDataStore)?.flush() + + XCTAssertEqual(dataInA?.utf8String, "feature A data") + XCTAssertEqual(dataInB?.utf8String, "feature B data") + } + + func testGivenFeatureRegisteredToTwoCoreInstances_whenWritingToDataStore_eachInstanceIsUnique() throws { + let coreDirectory1 = temporaryUniqueCoreDirectory().create() + let coreDirectory2 = temporaryUniqueCoreDirectory().create() + let core1 = DatadogCore( + directory: coreDirectory1, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockAny(), + backgroundTasksEnabled: .mockAny() + ) + let core2 = DatadogCore( + directory: coreDirectory2, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockAny(), + backgroundTasksEnabled: .mockAny() + ) + defer { + core1.flushAndTearDown() + core2.flushAndTearDown() + coreDirectory1.delete() + coreDirectory2.delete() + } + + // Given + try core1.register(feature: FeatureAMock()) + try core2.register(feature: FeatureAMock()) + + // When + let scope1 = core1.scope(for: FeatureAMock.self) + let scope2 = core2.scope(for: FeatureAMock.self) + + let commonKey = "key" + scope1.dataStore.setValue("feature data in core 1".utf8Data, forKey: commonKey) + scope2.dataStore.setValue("feature data in core 2".utf8Data, forKey: commonKey) + + // Then + var dataIn1: Data? + var dataIn2: Data? + scope1.dataStore.value(forKey: commonKey) { dataIn1 = $0.data() } + scope2.dataStore.value(forKey: commonKey) { dataIn2 = $0.data() } + + (scope1.dataStore as? FeatureDataStore)?.flush() + (scope2.dataStore as? FeatureDataStore)?.flush() + + XCTAssertEqual(dataIn1?.utf8String, "feature data in core 1") + XCTAssertEqual(dataIn2?.utf8String, "feature data in core 2") + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDirectoriesTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDirectoriesTests.swift new file mode 100644 index 0000000000..e1461de5ba --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCore+FeatureDirectoriesTests.swift @@ -0,0 +1,71 @@ +/* + * 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 +@testable import DatadogCore + +private struct RemoteFeatureMock: DatadogRemoteFeature { + static let name: String = "remote-feature-mock" + + var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() + var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() +} + +private struct FeatureMock: DatadogFeature { + static let name: String = "feature-mock" + + var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() +} + +class DatadogCore_FeatureDirectoriesTests: XCTestCase { + private var core: DatadogCore! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + temporaryCoreDirectory.create() + core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + temporaryCoreDirectory.delete() + super.tearDown() + } + + func testWhenRegisteringRemoteFeature_itCreatesFeatureDirectories() throws { + // When + try core.register(feature: RemoteFeatureMock()) + + // Then + let featureDirectory = try temporaryCoreDirectory.coreDirectory.subdirectory(path: RemoteFeatureMock.name) + XCTAssertNoThrow(try featureDirectory.subdirectory(path: "v2"), "Authorized data directory must exist") + XCTAssertNoThrow(try featureDirectory.subdirectory(path: "intermediate-v2"), "Intermediate data directory must exist") + } + + func testWhenRegisteringFeature_itDoesNotCreateFeatureDirectories() throws { + // When + try core.register(feature: FeatureMock()) + + // Then + XCTAssertThrowsError( + try temporaryCoreDirectory.coreDirectory.subdirectory(path: FeatureMock.name), + "Feature directory must not exist" + ) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift new file mode 100644 index 0000000000..acf94fe544 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/DatadogCoreTests.swift @@ -0,0 +1,333 @@ +/* + * 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 +@testable import DatadogCore + +private struct FeatureMock: DatadogRemoteFeature { + static let name: String = "mock" + + struct Event: Encodable { + let event: String + } + + var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() + var messageReceiver: FeatureMessageReceiver = FeatureMessageReceiverMock() + var performanceOverride: PerformancePresetOverride? = nil +} + +class DatadogCoreTests: XCTestCase { + override func setUp() { + super.setUp() + temporaryCoreDirectory.create() + } + + override func tearDown() { + temporaryCoreDirectory.delete() + super.tearDown() + } + + func testWhenWritingEventsWithDifferentTrackingConsent_itOnlyUploadsAuthorizedEvents() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + + let requestBuilderSpy = FeatureRequestBuilderSpy() + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + let scope = core.scope(for: FeatureMock.self) + + // When + core.set(trackingConsent: .notGranted) + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "not granted")) + } + + core.set(trackingConsent: .granted) + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "granted")) + } + + core.set(trackingConsent: .pending) + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "pending")) + } + + // Then + core.flushAndTearDown() + + let uploadedEvents = requestBuilderSpy.requestParameters + .flatMap { $0.events } + .map { $0.data.utf8String } + + XCTAssertEqual(uploadedEvents, [#"{"event":"granted"}"#], "Only `.granted` events should be uploaded") + 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( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + + let requestBuilderSpy = FeatureRequestBuilderSpy() + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + let scope = core.scope(for: FeatureMock.self) + + // When + core.set(trackingConsent: .notGranted) + scope.eventWriteContext(bypassConsent: true) { context, writer in + writer.write(value: FeatureMock.Event(event: "not granted")) + } + + core.set(trackingConsent: .granted) + scope.eventWriteContext(bypassConsent: true) { context, writer in + writer.write(value: FeatureMock.Event(event: "granted")) + } + + core.set(trackingConsent: .pending) + scope.eventWriteContext(bypassConsent: true) { context, writer in + writer.write(value: FeatureMock.Event(event: "pending")) + } + + // Then + core.flushAndTearDown() + + let uploadedEvents = requestBuilderSpy.requestParameters + .flatMap { $0.events } + .map { $0.data.utf8String } + + XCTAssertEqual( + uploadedEvents, + [ + #"{"event":"not granted"}"#, + #"{"event":"granted"}"#, + #"{"event":"pending"}"#, + ], + "It should upload all events" + ) + XCTAssertEqual(requestBuilderSpy.requestParameters.count, 1, "It should send only one request") + } + + func testWhenFeatureBaggageIsUpdated_thenNewValueIsImmediatellyAvailable() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .mockRandom(), + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + let feature = FeatureMock() + try core.register(feature: feature) + let scope = core.scope(for: FeatureMock.self) + + // When + let key = "key" + let expectation1 = self.expectation(description: "retrieve context") + let expectation2 = self.expectation(description: "retrieve context and event writer") + expectation1.expectedFulfillmentCount = 2 + expectation2.expectedFulfillmentCount = 2 + + core.set(baggage: "baggage 1", forKey: key) + scope.context { context in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 1") + expectation1.fulfill() + } + scope.eventWriteContext { context, _ in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 1") + expectation2.fulfill() + } + + core.set(baggage: "baggage 2", forKey: key) + scope.context { context in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 2") + expectation1.fulfill() + } + scope.eventWriteContext { context, _ in + XCTAssertEqual(try! context.baggages[key]!.decode(type: String.self), "baggage 2") + expectation2.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testWhenPerformancePresetOverrideIsProvided_itOverridesPresets() throws { + // Given + let core1 = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), + initialConsent: .granted, + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + let core2 = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: RelativeDateProvider(advancingBySeconds: 0.01), + initialConsent: .granted, + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + defer { + core1.flushAndTearDown() + core2.flushAndTearDown() + } + + // When + try core1.register( + feature: FeatureMock(performanceOverride: nil) + ) + try core2.register( + feature: FeatureMock( + performanceOverride: PerformancePresetOverride( + maxFileSize: 123, + maxObjectSize: 456, + meanFileAge: 100, + uploadDelay: nil + ) + ) + ) + + // Then + let storage1 = core1.stores.values.first?.storage + XCTAssertEqual(storage1?.authorizedFilesOrchestrator.performance.maxObjectSize, 512.KB.asUInt64()) + XCTAssertEqual(storage1?.authorizedFilesOrchestrator.performance.maxFileSize, 4.MB.asUInt64()) + + let storage2 = core2.stores.values.first?.storage + XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.maxObjectSize, 456) + XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.maxFileSize, 123) + XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.maxFileAgeForWrite, 95) + XCTAssertEqual(storage2?.authorizedFilesOrchestrator.performance.minFileAgeForRead, 105) + } + + func testWhenStoppingInstance_itDoesNotUploadEvents() throws { + // Given + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .mockRandom(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockAny(), + backgroundTasksEnabled: .mockAny() + ) + + let requestBuilderSpy = FeatureRequestBuilderSpy() + try core.register(feature: FeatureMock(requestBuilder: requestBuilderSpy)) + let scope = core.scope(for: FeatureMock.self) + + // When + core.stop() + + scope.eventWriteContext { context, writer in + writer.write(value: FeatureMock.Event(event: "should not be sent")) + } + + // Then + XCTAssertNil(core.get(feature: FeatureMock.self)) + core.flush() + XCTAssertEqual(requestBuilderSpy.requestParameters.count, 0, "It should not send any request") + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogCore/MessageBusTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/MessageBusTests.swift new file mode 100644 index 0000000000..fad2013431 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogCore/MessageBusTests.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 XCTest +import TestUtilities +import DatadogInternal + +@testable import DatadogCore + +class MessageBusTests: XCTestCase { + func testMessageBus() throws { + let expectation = XCTestExpectation(description: "dispatch message") + expectation.expectedFulfillmentCount = 2 + + // Given + let core = PassthroughCoreMock() + + let receiver = FeatureMessageReceiverMock(expectation: expectation) { message in + // Then + if let value: String = try? message.baggage(forKey: "test") { + XCTAssertEqual(value, "value") + expectation.fulfill() + } else { + XCTFail("wrong message case") + } + } + + let bus = MessageBus() + bus.connect(core: core) + + bus.connect(receiver, forKey: "receiver 1") + bus.connect(receiver, forKey: "receiver 2") + + // When + bus.send(message: .baggage(key: "test", value: "value")) + + // Then + wait(for: [expectation], timeout: 0.5) + bus.flush() + } + + func testItForwardConfigurationAfterDispatch() throws { + let expectation = XCTestExpectation(description: "dispatch configuration") + let receiver = FeatureMessageReceiverMock(expectation: expectation) { message in + guard + case .telemetry(let telemetry) = message, + case .configuration(let configuration) = telemetry + else { + return XCTFail("Message bus should send configuration telemetry") + } + + XCTAssertEqual(configuration.batchSize, 1) + XCTAssertTrue(configuration.trackErrors ?? false) + expectation.fulfill() + } + + // Given + let core = PassthroughCoreMock() + let bus = MessageBus(configurationDispatchTime: .milliseconds(90)) + bus.connect(core: core) + bus.connect(receiver, forKey: "test") + + // When + bus.configuration(batchSize: 1) + bus.configuration(trackErrors: true) + + // Then + wait(for: [expectation], timeout: 0.5) + bus.flush() + } +} + +extension MessageBus: Telemetry { + public func send(telemetry: DatadogInternal.TelemetryMessage) { + send(message: .telemetry(telemetry)) + } +} diff --git a/DatadogCore/Tests/Datadog/DatadogTests.swift b/DatadogCore/Tests/Datadog/DatadogTests.swift new file mode 100644 index 0000000000..221d3f0232 --- /dev/null +++ b/DatadogCore/Tests/Datadog/DatadogTests.swift @@ -0,0 +1,471 @@ +/* + * 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 DatadogLogs +@testable import DatadogTrace +@testable import DatadogCore + +class DatadogTests: XCTestCase { + private var printFunction: PrintFunctionMock! // swiftlint:disable:this implicitly_unwrapped_optional + private var defaultConfig = Datadog.Configuration(clientToken: "abc-123", env: "tests") + + override func setUp() { + super.setUp() + + XCTAssertFalse(Datadog.isInitialized()) + printFunction = PrintFunctionMock() + consolePrint = printFunction.print + } + + override func tearDown() { + consolePrint = { message, _ in print(message) } + printFunction = nil + XCTAssertFalse(Datadog.isInitialized()) + super.tearDown() + } + + // MARK: - Initializing with different configurations + + func testDefaultConfiguration() throws { + var configuration = defaultConfig + + configuration.bundle = .mockWith( + bundleIdentifier: "test", + CFBundleShortVersionString: "1.0.0", + CFBundleExecutable: "Test" + ) + + XCTAssertEqual(configuration.batchSize, .medium) + XCTAssertEqual(configuration.uploadFrequency, .average) + XCTAssertEqual(configuration.additionalConfiguration.count, 0) + XCTAssertNil(configuration.encryption) + XCTAssertTrue(configuration.serverDateProvider is DatadogNTPDateProvider) + + Datadog.initialize( + with: configuration, + trackingConsent: .granted + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let urlSessionClient = try XCTUnwrap(core.httpClient as? URLSessionClient) + XCTAssertTrue(core.dateProvider is SystemDateProvider) + XCTAssertNil(urlSessionClient.session.configuration.connectionProxyDictionary) + XCTAssertNil(core.encryption) + + let context = core.contextProvider.read() + XCTAssertEqual(context.clientToken, "abc-123") + XCTAssertEqual(context.env, "tests") + XCTAssertEqual(context.site, .us1) + XCTAssertEqual(context.service, "test") + XCTAssertEqual(context.version, "1.0.0") + XCTAssertEqual(context.sdkVersion, __sdkVersion) + XCTAssertEqual(context.applicationName, "Test") + XCTAssertNil(context.variant) + XCTAssertEqual(context.source, "ios") + XCTAssertEqual(context.applicationBundleIdentifier, "test") + XCTAssertEqual(context.trackingConsent, .granted) + } + + func testAdvancedConfiguration() throws { + var configuration = defaultConfig + + configuration.service = "service-name" + configuration.site = .eu1 + configuration.batchSize = .small + configuration.uploadFrequency = .frequent + configuration.proxyConfiguration = [ + kCFNetworkProxiesHTTPEnable: true, + kCFNetworkProxiesHTTPPort: 123, + kCFNetworkProxiesHTTPProxy: "www.example.com", + kCFProxyUsernameKey: "proxyuser", + kCFProxyPasswordKey: "proxypass", + ] + configuration.bundle = .mockWith( + bundleIdentifier: "test", + CFBundleShortVersionString: "1.0.0", + CFBundleExecutable: "Test" + ) + configuration.encryption = DataEncryptionMock() + configuration.serverDateProvider = ServerDateProviderMock() + configuration._internal_mutation { + $0.additionalConfiguration = [ + CrossPlatformAttributes.ddsource: "cp-source", + CrossPlatformAttributes.variant: "cp-variant", + CrossPlatformAttributes.sdkVersion: "cp-version" + ] + } + + XCTAssertEqual(configuration.batchSize, .small) + XCTAssertEqual(configuration.uploadFrequency, .frequent) + XCTAssertTrue(configuration.encryption is DataEncryptionMock) + XCTAssertTrue(configuration.serverDateProvider is ServerDateProviderMock) + + Datadog.initialize( + with: configuration, + trackingConsent: .pending + ) + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + XCTAssertTrue(core.dateProvider is SystemDateProvider) + XCTAssertTrue(core.encryption is DataEncryptionMock) + + let urlSessionClient = try XCTUnwrap(core.httpClient as? URLSessionClient) + let connectionProxyDictionary = try XCTUnwrap(urlSessionClient.session.configuration.connectionProxyDictionary) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPEnable] as? Bool, true) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPPort] as? Int, 123) + XCTAssertEqual(connectionProxyDictionary[kCFNetworkProxiesHTTPProxy] as? String, "www.example.com") + XCTAssertEqual(connectionProxyDictionary[kCFProxyUsernameKey] as? String, "proxyuser") + XCTAssertEqual(connectionProxyDictionary[kCFProxyPasswordKey] as? String, "proxypass") + + let context = core.contextProvider.read() + XCTAssertEqual(context.clientToken, "abc-123") + XCTAssertEqual(context.env, "tests") + XCTAssertEqual(context.site, .eu1) + XCTAssertEqual(context.service, "service-name") + XCTAssertEqual(context.version, "1.0.0") + XCTAssertEqual(context.sdkVersion, "cp-version") + XCTAssertEqual(context.applicationName, "Test") + XCTAssertEqual(context.variant, "cp-variant") + XCTAssertEqual(context.source, "cp-source") + XCTAssertEqual(context.applicationBundleIdentifier, "test") + XCTAssertEqual(context.trackingConsent, .pending) + } + + func testGivenDefaultConfiguration_itCanBeInitialized() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + XCTAssertTrue(Datadog.isInitialized()) + Datadog.flushAndDeinitialize() + } + + func testGivenInvalidConfiguration_itPrintsError() { + let invalidConfiguration = Datadog.Configuration(clientToken: "", env: "tests") + + Datadog.initialize( + with: invalidConfiguration, + trackingConsent: .mockRandom() + ) + + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: `clientToken` cannot be empty." + ) + XCTAssertFalse(Datadog.isInitialized()) + } + + func testGivenValidConfiguration_whenInitializedMoreThanOnce_itPrintsError() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: The 'main' instance of SDK is already initialized." + ) + + Datadog.flushAndDeinitialize() + } + + // MARK: - Public APIs + + func testTrackingConsent() { + let initialConsent: TrackingConsent = .mockRandom() + let nextConsent: TrackingConsent = .mockRandom() + + Datadog.initialize( + with: defaultConfig, + trackingConsent: initialConsent + ) + + let core = CoreRegistry.default as? DatadogCore + XCTAssertEqual(core?.consentPublisher.consent, initialConsent) + + Datadog.set(trackingConsent: nextConsent) + + XCTAssertEqual(core?.consentPublisher.consent, nextConsent) + + Datadog.flushAndDeinitialize() + } + + func testUserInfo() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + let core = CoreRegistry.default as? DatadogCore + + XCTAssertNil(core?.userInfoPublisher.current.id) + XCTAssertNil(core?.userInfoPublisher.current.email) + XCTAssertNil(core?.userInfoPublisher.current.name) + XCTAssertEqual(core?.userInfoPublisher.current.extraInfo as? [String: Int], [:]) + + Datadog.setUserInfo( + id: "foo", + name: "bar", + email: "foo@bar.com", + extraInfo: ["abc": 123] + ) + + XCTAssertEqual(core?.userInfoPublisher.current.id, "foo") + XCTAssertEqual(core?.userInfoPublisher.current.name, "bar") + XCTAssertEqual(core?.userInfoPublisher.current.email, "foo@bar.com") + XCTAssertEqual(core?.userInfoPublisher.current.extraInfo as? [String: Int], ["abc": 123]) + + Datadog.flushAndDeinitialize() + } + + func testAddUserPreoprties_mergesProperties() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + let core = CoreRegistry.default as? DatadogCore + + Datadog.setUserInfo( + id: "foo", + name: "bar", + email: "foo@bar.com", + extraInfo: ["abc": 123] + ) + + Datadog.addUserExtraInfo(["second": 667]) + + XCTAssertEqual(core?.userInfoPublisher.current.id, "foo") + XCTAssertEqual(core?.userInfoPublisher.current.name, "bar") + XCTAssertEqual(core?.userInfoPublisher.current.email, "foo@bar.com") + XCTAssertEqual( + core?.userInfoPublisher.current.extraInfo as? [String: Int], + ["abc": 123, "second": 667] + ) + + Datadog.flushAndDeinitialize() + } + + func testAddUserPreoprties_removesProperties() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + let core = CoreRegistry.default as? DatadogCore + + Datadog.setUserInfo( + id: "foo", + name: "bar", + email: "foo@bar.com", + extraInfo: ["abc": 123] + ) + + Datadog.addUserExtraInfo(["abc": nil, "second": 667]) + + XCTAssertEqual(core?.userInfoPublisher.current.id, "foo") + XCTAssertEqual(core?.userInfoPublisher.current.name, "bar") + XCTAssertEqual(core?.userInfoPublisher.current.email, "foo@bar.com") + XCTAssertEqual(core?.userInfoPublisher.current.extraInfo as? [String: Int], ["second": 667]) + + Datadog.flushAndDeinitialize() + } + + func testAddUserPreoprties_overwritesProperties() { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + let core = CoreRegistry.default as? DatadogCore + + Datadog.setUserInfo( + id: "foo", + name: "bar", + email: "foo@bar.com", + extraInfo: ["abc": 123] + ) + + Datadog.addUserExtraInfo(["abc": 444]) + + XCTAssertEqual(core?.userInfoPublisher.current.id, "foo") + XCTAssertEqual(core?.userInfoPublisher.current.name, "bar") + XCTAssertEqual(core?.userInfoPublisher.current.email, "foo@bar.com") + XCTAssertEqual(core?.userInfoPublisher.current.extraInfo as? [String: Int], ["abc": 444]) + + Datadog.flushAndDeinitialize() + } + + func testDefaultVerbosityLevel() { + XCTAssertNil(Datadog.verbosityLevel) + } + + func testGivenDataStoredInAllFeatureDirectories_whenClearAllDataIsUsed_allFilesAreRemoved() throws { + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + Logs.enable() + Trace.enable() + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + + // On SDK init, underlying `ConsentAwareDataWriter` performs data migration for each feature, which includes + // data removal in `unauthorised` (`.pending`) directory. To not cause test flakiness, we must ensure that + // mock data is written only after this operation completes - otherwise, migration may delete mocked files. + core.readWriteQueue.sync {} + + // Given + let featureDirectories: [FeatureDirectories] = [ + try core.directory.getFeatureDirectories(forFeatureNamed: "logging"), + try core.directory.getFeatureDirectories(forFeatureNamed: "tracing"), + ] + + 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 + Datadog.clearAllData() + + // Wait for async clear completion in all features: + core.readWriteQueue.sync {} + + // Then + 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() + } + + func testServerDateProvider() throws { + // Given + var config = defaultConfig + let serverDateProvider = ServerDateProviderMock() + config.serverDateProvider = serverDateProvider + + // When + Datadog.initialize( + with: config, + trackingConsent: .mockRandom() + ) + + serverDateProvider.offset = -1 + + // Then + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + let context = core.contextProvider.read() + XCTAssertEqual(context.serverTimeOffset, -1) + + Datadog.flushAndDeinitialize() + } + + func testRemoveV1DeprecatedFolders() throws { + // Given + let cache = try Directory.cache() + let directories = ["com.datadoghq.logs", "com.datadoghq.traces", "com.datadoghq.rum"] + try directories.forEach { + _ = try cache.createSubdirectory(path: $0).createFile(named: "test") + } + + // When + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + + defer { Datadog.flushAndDeinitialize() } + + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + // Wait for async deletion + core.readWriteQueue.sync {} + + // Then + XCTAssertThrowsError(try cache.subdirectory(path: "com.datadoghq.logs")) + XCTAssertThrowsError(try cache.subdirectory(path: "com.datadoghq.traces")) + XCTAssertThrowsError(try cache.subdirectory(path: "com.datadoghq.rum")) + } + + func testCustomSDKInstance() throws { + // When + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom(), + instanceName: "test" + ) + + defer { Datadog.flushAndDeinitialize(instanceName: "test") } + + // Then + XCTAssertTrue(CoreRegistry.default is NOPDatadogCore) + XCTAssertTrue(CoreRegistry.instance(named: "test") is DatadogCore) + } + + func testStopSDKInstance() throws { + // Given + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom(), + instanceName: "test" + ) + + // Then + XCTAssertTrue(CoreRegistry.instance(named: "test") is DatadogCore) + + // When + Datadog.stopInstance(named: "test") + + // Then + XCTAssertTrue(CoreRegistry.instance(named: "test") is NOPDatadogCore) + } + + func testGivenDefaultSDKInstanceInitialized_customOneCanBeInitializedAfterIt() throws { + let defaultConfig = Datadog.Configuration(clientToken: "abc-123", env: "default") + let customConfig = Datadog.Configuration(clientToken: "def-456", env: "custom") + + // Given + Datadog.initialize( + with: defaultConfig, + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + // When + Datadog.initialize( + with: customConfig, + trackingConsent: .mockRandom(), + instanceName: "custom-instance" + ) + defer { Datadog.flushAndDeinitialize(instanceName: "custom-instance") } + + // Then + XCTAssertTrue(CoreRegistry.default is DatadogCore) + XCTAssertTrue(CoreRegistry.instance(named: "custom-instance") is DatadogCore) + } +} diff --git a/DatadogCore/Tests/Datadog/FeaturesIntegration/CITestIntegrationTests.swift b/DatadogCore/Tests/Datadog/FeaturesIntegration/CITestIntegrationTests.swift new file mode 100644 index 0000000000..bd34fda687 --- /dev/null +++ b/DatadogCore/Tests/Datadog/FeaturesIntegration/CITestIntegrationTests.swift @@ -0,0 +1,14 @@ +/* + * 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. + */ + +@testable import DatadogCore +import XCTest + +class CITestIntegrationTests: XCTestCase { + func testByDefaultCITestIntegrationIsNotConfigured() throws { + XCTAssertNil(CITestIntegration.active) + } +} diff --git a/DatadogCore/Tests/Datadog/FeaturesIntegration/TracingWithLoggingIntegrationTests.swift b/DatadogCore/Tests/Datadog/FeaturesIntegration/TracingWithLoggingIntegrationTests.swift new file mode 100644 index 0000000000..9032b3d944 --- /dev/null +++ b/DatadogCore/Tests/Datadog/FeaturesIntegration/TracingWithLoggingIntegrationTests.swift @@ -0,0 +1,138 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogLogs +@testable import DatadogTrace +@testable import DatadogCore + +class TracingWithLoggingIntegrationTests: XCTestCase { + private var core: PassthroughCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = PassthroughCoreMock(messageReceiver: LogMessageReceiver.mockAny()) + } + + override func tearDown() { + core = nil + super.tearDown() + } + + func testSendingLogWithOTMessageField() throws { + core.expectation = expectation(description: "Send log") + + // Given + let integration = TracingWithLoggingIntegration(core: core, service: .mockAny(), networkInfoEnabled: .mockAny()) + + // When + integration.writeLog( + withSpanContext: .mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200), + fields: [ + OTLogFields.message: "hello", + "custom field": 123, + ], + date: .mockDecember15th2019At10AMUTC(), + else: {} + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send log") + XCTAssertEqual(log.date, .mockDecember15th2019At10AMUTC()) + XCTAssertEqual(log.status, .info) + XCTAssertEqual(log.message, "hello") + DDAssertJSONEqual( + AnyEncodable(log.attributes.userAttributes), + AnyEncodable(["custom field": 123]) + ) + DDAssertJSONEqual( + AnyEncodable(log.attributes.internalAttributes), + AnyEncodable([ + "dd.trace_id": "a0000000000000064", + "dd.span_id": "c8" + ]) + ) + } + + func testWritingLogWithOTErrorField() throws { + core.expectation = expectation(description: "Send 3 logs") + core.expectation?.expectedFulfillmentCount = 3 + + // Given + let integration = TracingWithLoggingIntegration(core: core, service: .mockAny(), networkInfoEnabled: .mockAny()) + + // When + integration.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.event: "error"], + date: .mockAny(), + else: {} + ) + + integration.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.errorKind: "Swift error"], + date: .mockAny(), + else: {} + ) + + integration.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.event: "error", OTLogFields.errorKind: "Swift error"], + date: .mockAny(), + else: {} + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let logs: [LogEvent] = try XCTUnwrap(core.events()) + XCTAssertEqual(logs.count, 3, "It should send 3 logs") + logs.forEach { log in + XCTAssertEqual(log.status, .error) + XCTAssertEqual(log.message, "Span event") + } + } + + func testWritingCustomLogWithoutAnyOTFields() throws { + core.expectation = expectation(description: "Send log") + + // Given + let integration = TracingWithLoggingIntegration(core: core, service: .mockAny(), networkInfoEnabled: .mockAny()) + + // When + integration.writeLog( + withSpanContext: .mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200), + fields: ["custom field": 123], + date: .mockDecember15th2019At10AMUTC(), + else: {} + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send log") + XCTAssertEqual(log.date, .mockDecember15th2019At10AMUTC()) + XCTAssertEqual(log.status, .info) + XCTAssertEqual(log.message, "Span event", "It should use default message.") + DDAssertJSONEqual( + AnyEncodable(log.attributes.userAttributes), + AnyEncodable(["custom field": 123]) + ) + DDAssertJSONEqual( + AnyEncodable(log.attributes.internalAttributes), + AnyEncodable([ + "dd.trace_id": "a0000000000000064", + "dd.span_id": "c8" + ]) + ) + } +} diff --git a/DatadogCore/Tests/Datadog/InternalProxyTests.swift b/DatadogCore/Tests/Datadog/InternalProxyTests.swift new file mode 100644 index 0000000000..4d9cb18a4d --- /dev/null +++ b/DatadogCore/Tests/Datadog/InternalProxyTests.swift @@ -0,0 +1,105 @@ +/* + * 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 XCTest +import TestUtilities +import DatadogInternal +@testable import DatadogCore + +class InternalProxyTests: 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 testProxyDebugCallsTelemetryDebug() throws { + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // Given + let id: String = .mockAny() + let message: String = .mockAny() + + // When + Datadog._internal.telemetry.debug(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 testProxyErrorCallsTelemetryError() 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 + Datadog._internal.telemetry.error(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 testWhenTelemetryIsSentThroughProxy_thenItForwardsToDDTelemetry() throws { + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // When + let randomDebugMessage: String = .mockRandom() + let randomErrorMessage: String = .mockRandom() + Datadog._internal.telemetry.debug(id: .mockAny(), message: randomDebugMessage) + Datadog._internal.telemetry.error(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) + } + + func testWhenNewVersionIsSetInConfigurationProxy_thenItChangesAppVersionInCore() throws { + // Given + Datadog.initialize( + with: .mockAny(), + trackingConsent: .mockRandom() + ) + defer { Datadog.flushAndDeinitialize() } + + // When + let randomVersion: String = .mockRandom() + Datadog._internal.set(customVersion: randomVersion) + + // Then + let core = try XCTUnwrap(CoreRegistry.default as? DatadogCore) + XCTAssertEqual(core.applicationVersionPublisher.version, randomVersion) + } +} diff --git a/DatadogCore/Tests/Datadog/Kronos/KronosInternetAddressTests.swift b/DatadogCore/Tests/Datadog/Kronos/KronosInternetAddressTests.swift new file mode 100644 index 0000000000..d4e56dbf42 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Kronos/KronosInternetAddressTests.swift @@ -0,0 +1,120 @@ +/* + * 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 DatadogCore + +class KronosInternetAddressTests: XCTestCase { + func testIfIPv4AddressIsPrivate() throws { + let privateIPs: [KronosInternetAddress] = try (0..<50).flatMap { _ in + return [ + // random private IPs of class A: 10.0.0.0 — 10.255.255.255 + try .mockIPv4([10, .mockRandom(), .mockRandom(), .mockRandom()]), + // random private IPs of class B: 172.16.0.0 — 172.31.255.255 + try .mockIPv4([172, .mockRandom(min: 16, max: 31), .mockRandom(), .mockRandom()]), + // random private IPs of class C: 192.168.0.0 — 192.168.255.255 + try .mockIPv4([192, 168, .mockRandom(), .mockRandom()]), + // multicast IPs 224.0.0.0 - 239.255.255.255 + try .mockIPv4([.mockRandom(min: 224, max: 239), .mockRandom(), .mockRandom(), .mockRandom()]), + // broadcast IP 255.255.255.255 + try .mockIPv4([255, 255, 255, 255]), + ] + } + let publicIPs: [KronosInternetAddress] = try (0..<50).flatMap { _ in + return [ + try .mockIPv4([.mockRandom(otherThan: Set([10, 172, 192, 255] + (224...239))), .mockRandom(), .mockRandom(), .mockRandom()]), + try .mockIPv4([172, .mockRandom(min: 0, max: 15), .mockRandom(), .mockRandom()]), + try .mockIPv4([172, .mockRandom(min: 32, max: 255), .mockRandom(), .mockRandom()]), + try .mockIPv4([192, .mockRandom(otherThan: [168]), .mockRandom(), .mockRandom()]), + try .mockIPv4([255, .mockRandom(max: 254), .mockRandom(), .mockRandom()]), + try .mockIPv4([255, .mockRandom(), .mockRandom(max: 254), .mockRandom()]), + try .mockIPv4([255, .mockRandom(), .mockRandom(), .mockRandom(max: 254)]), + ] + } + + privateIPs.forEach { ip in + XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP") + } + publicIPs.forEach { ip in + XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP") + } + } + + func testIfIPv6AddressIsPrivate() throws { + let privateIPs: [KronosInternetAddress] = try (0..<50).flatMap { _ in + return [ + // random private IP starting with `fd` prefix + try .mockIPv6([0xfd] + (0..<15).map({ _ in .mockRandom() })), + // random multicast IP starting with `ff` prefix + try .mockIPv6([0xff] + (0..<15).map({ _ in .mockRandom() })), + ] + } + let publicIPs: [KronosInternetAddress] = try (0..<50).flatMap { _ in + return [ + // first byte is mocked to avoid having `fd` or `ff` prefix + try .mockIPv6([.mockRandom(min: 0xf0, otherThan: [0xfd, 0xff])] + (0..<15).map({ _ in .mockRandom() })), + try .mockIPv6([.mockRandom(max: 0xfc, otherThan: [0xf])] + (0..<15).map({ _ in .mockRandom() })), + ] + } + + privateIPs.forEach { ip in + XCTAssertTrue(ip.isPrivate, "\(ip.host ?? "nil") should be private IP") + } + publicIPs.forEach { ip in + XCTAssertFalse(ip.isPrivate, "\(ip.host ?? "nil") should not be private IP") + } + } +} + +// MARK: - Mocks + +private extension KronosInternetAddress { + static func mockIPv4(_ bytes: [UInt8]) throws -> KronosInternetAddress { + precondition(bytes.count == 4, "Expected 4 bytes") + let numbers = bytes.map { String($0) } + let ipv4String = numbers.joined(separator: ".") // e.g. '192.168.1.1' + let address: KronosInternetAddress? = .mockWith(ipv4String: ipv4String) + return try XCTUnwrap(address, "\(ipv4String) is not a valid IPv4 string") + } + + static func mockIPv6(_ bytes: [UInt8]) throws -> KronosInternetAddress { + precondition(bytes.count == 16, "Expected 16 bytes") + let groups: [String] = (0..<8).map { idx in + let hexA = String(bytes[idx * 2], radix: 16) + let hexB = String(bytes[idx * 2 + 1], radix: 16) + return hexA + hexB + } + let ipv6String = groups.joined(separator: ":") // e.g. 'ab:ab:ab:ab:ab:ab:ab:ab' + let address: KronosInternetAddress? = .mockWith(ipv6String: ipv6String.randomcased()) + return try XCTUnwrap(address, "\(ipv6String) is not a valid IPv6 string") + } + + static func mockWith(ipv4String: String) -> KronosInternetAddress? { + var inaddr = in_addr() + guard ipv4String.withCString({ inet_pton(AF_INET, $0, &inaddr) }) == 1 else { + return nil // likely, not an IPv4 string + } + + var addr = sockaddr_in() + addr.sin_len = UInt8(MemoryLayout.size(ofValue: addr)) + addr.sin_family = sa_family_t(AF_INET) + addr.sin_addr = inaddr + return .ipv4(addr) + } + + static func mockWith(ipv6String: String) -> KronosInternetAddress? { + var inaddr = in6_addr() + guard ipv6String.withCString({ inet_pton(AF_INET6, $0, &inaddr) }) == 1 else { + return nil // likely, not an IPv6 string + } + + var addr = sockaddr_in6() + addr.sin6_len = UInt8(MemoryLayout.size(ofValue: addr)) + addr.sin6_family = sa_family_t(AF_INET6) + addr.sin6_addr = inaddr + return .ipv6(addr) + } +} diff --git a/DatadogCore/Tests/Datadog/Kronos/KronosNTPPacketTests.swift b/DatadogCore/Tests/Datadog/Kronos/KronosNTPPacketTests.swift new file mode 100644 index 0000000000..5a9b56cb5b --- /dev/null +++ b/DatadogCore/Tests/Datadog/Kronos/KronosNTPPacketTests.swift @@ -0,0 +1,50 @@ +/* + * 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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import DatadogCore + +final class KronosNTPPacketTests: XCTestCase { + func testToData() { + var packet = KronosNTPPacket() + let data = packet.prepareToSend(transmitTime: 1_463_303_662.776552) + XCTAssertEqual(data, Data(hex: "1b0004fa0001000000010000000000000000000000000000" + + "00000000000000000000000000000000dae2bc6ec6cc1c00")!) + } + + func testParseInvalidData() { + let network = Data(hex: "0badface")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertNil(PDU) + } + + func testParseData() { + let network = Data(hex: "1c0203e90000065700000a68ada2c09cdae2d084a5a76d5fdae2d3354a529000dae2d32b" + + "b38bab46dae2d32bb38d9e00")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertEqual(PDU?.version, 3) + XCTAssertEqual(PDU?.leap, KronosLeapIndicator.noWarning) + XCTAssertEqual(PDU?.mode, KronosMode.server) + XCTAssertEqual(PDU?.stratum, KronosStratum.secondary) + XCTAssertEqual(PDU?.poll, 3) + XCTAssertEqual(PDU?.precision, -23) + } + + func testParseTimeData() { + let network = Data(hex: "1c0203e90000065700000a68ada2c09cdae2d084a5a76d5fdae2d3354a529000dae2d32b" + + "b38bab46dae2d32bb38d9e00")! + let PDU = try? KronosNTPPacket(data: network, destinationTime: 0) + XCTAssertEqual(PDU?.rootDelay, 0.0247650146484375) + XCTAssertEqual(PDU?.rootDispersion, 0.0406494140625) + XCTAssertEqual(PDU?.clockSource.ID, 2_913_124_508) + XCTAssertEqual(PDU?.referenceTime, 1_463_308_804.6470859051) + XCTAssertEqual(PDU?.originTime, 1_463_309_493.2903223038) + XCTAssertEqual(PDU?.receiveTime, 1_463_309_483.7013499737) + } +} diff --git a/DatadogCore/Tests/Datadog/Kronos/KronosTimeStorageTests.swift b/DatadogCore/Tests/Datadog/Kronos/KronosTimeStorageTests.swift new file mode 100644 index 0000000000..9d993e1774 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Kronos/KronosTimeStorageTests.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. + * + * This file includes software developed by MobileNativeFoundation, https://mobilenativefoundation.org and altered by Datadog. + * Use of this source code is governed by Apache License 2.0 license: https://github.com/MobileNativeFoundation/Kronos/blob/main/LICENSE + */ + +import XCTest +@testable import DatadogCore + +class KronosTimeStoragePolicyTests: XCTestCase { + func testInitWithStringGivesAppGroupType() { + let group = KronosTimeStoragePolicy(appGroupID: "com.test.something.mygreatapp") + if case KronosTimeStoragePolicy.appGroup(_) = group { + XCTAssert(true) + } else { + XCTAssert(false) + } + } + + func testInitWithNIlGivesStandardType() { + let group = KronosTimeStoragePolicy(appGroupID: nil) + if case KronosTimeStoragePolicy.standard = group { + XCTAssert(true) + } else { + XCTAssert(false) + } + } +} + +class KronosTimeStorageTests: XCTestCase { + func testStoringAndRetrievingTimeFreeze() { + var storage = KronosTimeStorage(storagePolicy: .standard) + let sampleFreeze = KronosTimeFreeze(offset: 5_000.32423) + storage.stableTime = sampleFreeze + + let fromDefaults = storage.stableTime + XCTAssertNotNil(fromDefaults) + XCTAssertEqual(sampleFreeze.toDictionary(), fromDefaults!.toDictionary()) + } + + func testRetrievingTimeFreezeAfterReboot() { + let sampleFreeze = KronosTimeFreeze(offset: 5_000.32423) + var storedData = sampleFreeze.toDictionary() + storedData["Uptime"] = storedData["Uptime"]! + 10 + + let beforeRebootFreeze = KronosTimeFreeze(from: sampleFreeze.toDictionary()) + let afterRebootFreeze = KronosTimeFreeze(from: storedData) + XCTAssertNil(afterRebootFreeze) + XCTAssertNotNil(beforeRebootFreeze) + } +} diff --git a/DatadogCore/Tests/Datadog/LoggerTests.swift b/DatadogCore/Tests/Datadog/LoggerTests.swift new file mode 100644 index 0000000000..96edc86dff --- /dev/null +++ b/DatadogCore/Tests/Datadog/LoggerTests.swift @@ -0,0 +1,1017 @@ +/* + * 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 OpenTelemetryApi + +@testable import DatadogLogs +@testable import DatadogTrace +@testable import DatadogRUM +@testable import DatadogCore + +// swiftlint:disable multiline_arguments_brackets +class LoggerTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + super.tearDown() + } + + // MARK: - Customizing Logger + + func testSendingLogWithDefaultLogger() throws { + core.context = .mockWith( + service: "default-service-name", + env: "tests", + version: "1.0.0", + buildNumber: "1", + sdkVersion: "1.2.3", + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + device: .mockWith( + name: "Device Name", + model: "Model Name", + osName: "testOS", + osVersion: "1.0", + osBuildNumber: "FFFFFF", + architecture: "testArch" + ) + ) + + let feature: LogsFeature = .mockWith( + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + ) + try core.register(feature: feature) + + let logger = Logger.create(in: core) + logger.debug("message") + + let logMatcher = try core.waitAndReturnLogMatchers()[0] + try logMatcher.assertItFullyMatches(jsonString: """ + { + "status" : "debug", + "message" : "message", + "os": { + "build": "FFFFFF", + "name": "testOS", + "version": "1.0" + }, + "service" : "default-service-name", + "logger.name" : "com.datadoghq.ios-sdk", + "logger.version": "1.2.3", + "logger.thread_name" : "main", + "date" : "2019-12-15T10:00:00.000Z", + "version": "1.0.0", + "build_version": "1", + "ddtags": "env:tests,version:1.0.0", + "_dd": { + "device": { + "brand": "Apple", + "name": "Device Name", + "model": "Model Name", + "architecture": "testArch" + } + } + } + """) + } + + func testSendingLogWithCustomizedLogger() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create( + with: Logger.Configuration( + service: "custom-service-name", + name: "custom-logger-name", + networkInfoEnabled: true, + consoleLogFormat: .short + ), + in: core + ) + + logger.debug("message") + + let logMatcher = try core.waitAndReturnLogMatchers()[0] + + logMatcher.assertService(equals: "custom-service-name") + logMatcher.assertLoggerName(equals: "custom-logger-name") + logMatcher.assertValue(forKeyPath: "network.client.sim_carrier.name", isTypeOf: String.self) + logMatcher.assertValue(forKeyPath: "network.client.sim_carrier.iso_country", isTypeOf: String.self) + logMatcher.assertValue(forKeyPath: "network.client.sim_carrier.technology", isTypeOf: String.self) + logMatcher.assertValue(forKeyPath: "network.client.sim_carrier.allows_voip", isTypeOf: Bool.self) + logMatcher.assertValue(forKeyPath: "network.client.available_interfaces", isTypeOf: [String].self) + logMatcher.assertValue(forKeyPath: "network.client.reachability", isTypeOf: String.self) + logMatcher.assertValue(forKeyPath: "network.client.is_expensive", isTypeOf: Bool.self) + logMatcher.assertValue(forKeyPath: "network.client.supports_ipv4", isTypeOf: Bool.self) + logMatcher.assertValue(forKeyPath: "network.client.supports_ipv6", isTypeOf: Bool.self) + if #available(iOS 13.0, *) { + logMatcher.assertValue(forKeyPath: "network.client.is_constrained", isTypeOf: Bool.self) + } + } + + // MARK: - Sending Customized Logs + + func testSendingLogsWithDifferentDates() throws { + let feature: LogsFeature = .mockWith( + dateProvider: RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC(), advancingBySeconds: 1) + ) + try core.register(feature: feature) + + let logger = Logger.create(in: core) + logger.info("message 1") + logger.info("message 2") + logger.info("message 3") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC() }) + logMatchers[1].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 1) }) + logMatchers[2].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 2) }) + } + + func testSendingLogsWithDifferentLevels() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + logger.debug("message") + logger.info("message") + logger.notice("message") + logger.warn("message") + logger.error("message") + logger.critical("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") + } + + func testSendingLogsAboveCertainLevel() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(with: Logger.Configuration(remoteLogThreshold: .warn), in: core) + + logger.debug("message") + logger.info("message") + logger.notice("message") + logger.warn("message") + logger.error("message") + logger.critical("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertStatus(equals: "warn") + logMatchers[1].assertStatus(equals: "error") + logMatchers[2].assertStatus(equals: "critical") + } + + // MARK: - Logging an error + + func testLoggingError() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + struct TestError: Error { + var description = "Test description" + } + let error = TestError() + + let logger = Logger.create(in: core) + logger.debug("message", error: error) + logger.info("message", error: error) + logger.notice("message", error: error) + logger.warn("message", error: error) + logger.error("message", error: error) + logger.critical("message", error: error) + + let logMatchers = try core.waitAndReturnLogMatchers() + for matcher in logMatchers { + matcher.assertValue(forKeyPath: "error.stack", equals: "TestError(description: \"Test description\")") + matcher.assertValue(forKeyPath: "error.message", equals: "TestError(description: \"Test description\")") + matcher.assertValue(forKeyPath: "error.kind", equals: "TestError") + matcher.assertValue(forKeyPath: "error.source_type", equals: "ios") + } + } + + func testLoggingErrorStrings() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + let errorKind = String.mockRandom() + let errorMessage = String.mockRandom() + let stackTrace = String.mockRandom() + logger._internal.log(level: .info, + message: .mockAny(), + errorKind: errorKind, + errorMessage: errorMessage, + stackTrace: stackTrace, + attributes: nil + ) + + let logMatchers = try core.waitAndReturnLogMatchers() + let logMatcher = logMatchers.first + XCTAssertNotNil(logMatcher) + if let logMatcher = logMatcher { + logMatcher.assertValue(forKeyPath: "error.kind", equals: errorKind) + logMatcher.assertValue(forKeyPath: "error.message", equals: errorMessage) + logMatcher.assertValue(forKeyPath: "error.stack", equals: stackTrace) + logMatcher.assertValue(forKeyPath: "error.source_type", equals: "ios") + } + } + + func testLoggingErrorWithSourceType() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + let errorKind = String.mockRandom() + let errorMessage = String.mockRandom() + let stackTrace = String.mockRandom() + logger._internal.log(level: .info, + message: .mockAny(), + errorKind: errorKind, + errorMessage: errorMessage, + stackTrace: stackTrace, + attributes: [ + "_dd.error.source_type": "flutter" + ] + ) + + let logMatchers = try core.waitAndReturnLogMatchers() + let logMatcher = logMatchers.first + XCTAssertNotNil(logMatcher) + if let logMatcher = logMatcher { + logMatcher.assertValue(forKeyPath: "error.kind", equals: errorKind) + logMatcher.assertValue(forKeyPath: "error.message", equals: errorMessage) + logMatcher.assertValue(forKeyPath: "error.stack", equals: stackTrace) + logMatcher.assertValue(forKeyPath: "error.source_type", equals: "flutter") + } + } + + // MARK: - Sampling + + func testSamplingEnabled() throws { + core.context = .mockAny() + try core.register(feature: LogsFeature.mockAny()) + let logger = Logger.create(with: Logger.Configuration(remoteSampleRate: 100), in: core) + + logger.debug(.mockAny()) + logger.info(.mockAny()) + logger.notice(.mockAny()) + logger.warn(.mockAny()) + logger.error(.mockAny()) + logger.critical(.mockAny()) + + XCTAssertEqual(try core.waitAndReturnLogMatchers().count, 6) + } + + func testSamplingDisabled() throws { + core.context = .mockAny() + try core.register(feature: LogsFeature.mockAny()) + let logger = Logger.create(with: Logger.Configuration(remoteSampleRate: 0), in: core) + + logger.debug(.mockAny()) + logger.info(.mockAny()) + logger.notice(.mockAny()) + logger.warn(.mockAny()) + logger.error(.mockAny()) + logger.critical(.mockAny()) + + XCTAssertEqual(try core.waitAndReturnLogMatchers().count, 0) + } + + // MARK: - Sending user info + + func testSendingUserInfo() throws { + core.context = .mockWith( + userInfo: .empty + ) + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + logger.debug("message with no user info") + + core.context.userInfo = UserInfo(id: "abc-123", name: "Foo", email: nil, extraInfo: [:]) + logger.debug("message with user `id` and `name`") + + core.context.userInfo = UserInfo( + id: "abc-123", + name: "Foo", + email: "foo@example.com", + extraInfo: [ + "str": "value", + "int": 11_235, + "bool": true + ] + ) + logger.debug("message with user `id`, `name`, `email` and `extraInfo`") + + core.context.userInfo = .empty + logger.debug("message with no user info") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertUserInfo(equals: nil) + + logMatchers[1].assertUserInfo(equals: (id: "abc-123", name: "Foo", email: nil)) + + logMatchers[2].assertUserInfo( + equals: ( + id: "abc-123", + name: "Foo", + email: "foo@example.com" + ) + ) + logMatchers[2].assertValue(forKey: "usr.str", equals: "value") + logMatchers[2].assertValue(forKey: "usr.int", equals: 11_235) + logMatchers[2].assertValue(forKey: "usr.bool", equals: true) + + logMatchers[3].assertUserInfo(equals: nil) + } + + // MARK: - Sending carrier info + + func testSendingCarrierInfoWhenEnteringAndLeavingCellularServiceRange() throws { + core.context = .mockWith( + carrierInfo: nil + ) + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(with: Logger.Configuration(networkInfoEnabled: true), in: core) + + // simulate entering cellular service range + core.context.carrierInfo = .mockWith( + carrierName: "Carrier", + carrierISOCountryCode: "US", + carrierAllowsVOIP: true, + radioAccessTechnology: .LTE + ) + + logger.debug("message") + + // simulate leaving cellular service range + core.context.carrierInfo = nil + + logger.debug("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue(forKeyPath: "network.client.sim_carrier.name", equals: "Carrier") + logMatchers[0].assertValue(forKeyPath: "network.client.sim_carrier.iso_country", equals: "US") + logMatchers[0].assertValue(forKeyPath: "network.client.sim_carrier.technology", equals: "LTE") + logMatchers[0].assertValue(forKeyPath: "network.client.sim_carrier.allows_voip", equals: true) + logMatchers[1].assertNoValue(forKeyPath: "network.client.sim_carrier") + } + + // MARK: - Sending network info + + func testSendingNetworkConnectionInfoWhenReachabilityChanges() throws { + core.context = .mockWith( + networkConnectionInfo: nil + ) + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(with: Logger.Configuration(networkInfoEnabled: true), in: core) + + // simulate reachable network + core.context.networkConnectionInfo = .mockWith( + reachability: .yes, + availableInterfaces: [.wifi, .cellular], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: false, + isConstrained: false + ) + + logger.debug("message") + + // simulate unreachable network + core.context.networkConnectionInfo = .mockWith( + reachability: .no, + availableInterfaces: [], + supportsIPv4: false, + supportsIPv6: false, + isExpensive: false, + isConstrained: false + ) + + logger.debug("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue(forKeyPath: "network.client.reachability", equals: "yes") + logMatchers[0].assertValue(forKeyPath: "network.client.available_interfaces", equals: ["wifi", "cellular"]) + logMatchers[0].assertValue(forKeyPath: "network.client.is_constrained", equals: false) + logMatchers[0].assertValue(forKeyPath: "network.client.is_expensive", equals: false) + logMatchers[0].assertValue(forKeyPath: "network.client.supports_ipv4", equals: true) + logMatchers[0].assertValue(forKeyPath: "network.client.supports_ipv6", equals: true) + + logMatchers[1].assertValue(forKeyPath: "network.client.reachability", equals: "no") + logMatchers[1].assertValue(forKeyPath: "network.client.available_interfaces", equals: [String]()) + logMatchers[1].assertValue(forKeyPath: "network.client.is_constrained", equals: false) + logMatchers[1].assertValue(forKeyPath: "network.client.is_expensive", equals: false) + logMatchers[1].assertValue(forKeyPath: "network.client.supports_ipv4", equals: false) + logMatchers[1].assertValue(forKeyPath: "network.client.supports_ipv6", equals: false) + } + + // MARK: - Sending attributes + + func testSendingLoggerAttributesOfDifferentEncodableValues() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + // string literal + logger.addAttribute(forKey: "string", value: "hello") + + // boolean literal + logger.addAttribute(forKey: "bool", value: true) + + // integer literal + logger.addAttribute(forKey: "int", value: 10) + + // Typed 8-bit unsigned Integer + logger.addAttribute(forKey: "uint-8", value: UInt8(10)) + + // double-precision, floating-point value + logger.addAttribute(forKey: "double", value: 10.5) + + // array of `Encodable` integer + logger.addAttribute(forKey: "array-of-int", value: [1, 2, 3]) + + // dictionary of `Encodable` date types + logger.addAttribute(forKey: "dictionary-of-date", value: [ + "date1": Date.mockDecember15th2019At10AMUTC(), + "date2": Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 60 * 60) + ]) + + struct Person: Codable { + let name: String + let age: Int + let nationality: String + } + + // custom `Encodable` structure + logger.addAttribute(forKey: "person", value: Person(name: "Adam", age: 30, nationality: "Polish")) + + // nested string literal + logger.addAttribute(forKey: "nested.string", value: "hello") + + // URL + logger.addAttribute(forKey: "url", value: URL(string: "https://example.com/image.png")!) + + logger.info("message") + + let logMatcher = try core.waitAndReturnLogMatchers()[0] + logMatcher.assertValue(forKey: "string", equals: "hello") + logMatcher.assertValue(forKey: "bool", equals: true) + logMatcher.assertValue(forKey: "int", equals: 10) + logMatcher.assertValue(forKey: "uint-8", equals: UInt8(10)) + logMatcher.assertValue(forKey: "double", equals: 10.5) + logMatcher.assertValue(forKey: "array-of-int", equals: [1, 2, 3]) + logMatcher.assertValue(forKeyPath: "dictionary-of-date.date1", equals: "2019-12-15T10:00:00.000Z") + logMatcher.assertValue(forKeyPath: "dictionary-of-date.date2", equals: "2019-12-15T11:00:00.000Z") + logMatcher.assertValue(forKeyPath: "person.name", equals: "Adam") + logMatcher.assertValue(forKeyPath: "person.age", equals: 30) + logMatcher.assertValue(forKeyPath: "person.nationality", equals: "Polish") + logMatcher.assertValue(forKeyPath: "nested.string", equals: "hello") + /// URLs are encoded explicitly as `String` - see the comment in `EncodableValue` + logMatcher.assertValue(forKeyPath: "url", equals: "https://example.com/image.png") + } + + func testSendingMessageAttributes() throws { + core.context = .mockAny() + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + // add logger attribute + logger.addAttribute(forKey: "attribute", value: "logger's value") + + // send message with no attributes + logger.info("info message 1") + + // send message with attribute overriding logger's attribute + logger.info("info message 2", attributes: ["attribute": "message's value"]) + + // remove logger attribute + logger.removeAttribute(forKey: "attribute") + + // send message + logger.info("info message 3") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue(forKey: "attribute", equals: "logger's value") + logMatchers[1].assertValue(forKey: "attribute", equals: "message's value") + logMatchers[2].assertNoValue(forKey: "attribute") + } + + // MARK: - Sending tags + + func testSendingTags() throws { + core.context = .mockWith( + env: "tests", + version: "1.2.3" + ) + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + // add tag + logger.add(tag: "tag1") + + // send message + logger.info("info message 1") + + // add tag with key + logger.addTag(withKey: "tag2", value: "abcd") + + // send message + logger.info("info message 2") + + // remove tag with key + logger.removeTag(withKey: "tag2") + + // remove tag + logger.remove(tag: "tag1") + + // send message + logger.info("info message 3") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertTags(equal: ["tag1", "env:tests", "version:1.2.3"]) + logMatchers[1].assertTags(equal: ["tag1", "tag2:abcd", "env:tests", "version:1.2.3"]) + logMatchers[2].assertTags(equal: ["env:tests", "version:1.2.3"]) + } + + func testSendingTagsWithVariant() throws { + core.context = .mockWith( + env: "tests", + version: "1.2.3", + variant: "integration" + ) + + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + // add tag + logger.add(tag: "tag1") + + // send message + logger.info("info message 1") + + // add tag with key + logger.addTag(withKey: "tag2", value: "abcd") + + // send message + logger.info("info message 2") + + // remove tag with key + logger.removeTag(withKey: "tag2") + + // remove tag + logger.remove(tag: "tag1") + + // send message + logger.info("info message 3") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertTags(equal: ["tag1", "env:tests", "version:1.2.3", "variant:integration"]) + logMatchers[1].assertTags(equal: ["tag1", "tag2:abcd", "env:tests", "version:1.2.3", "variant:integration"]) + logMatchers[2].assertTags(equal: ["env:tests", "version:1.2.3", "variant:integration"]) + } + + // MARK: - Integration With RUM Feature + + func testGivenBundlingWithRUMEnabledAndRUMFeatureEnabled_whenSendingLogBeforeAnyUserActivity_itContainsSessionId() throws { + core.context = .mockAny() + + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + RUM.enable( + with: .mockWith { $0.sessionSampleRate = .maxSampleRate }, + in: core + ) + + // given + let logger = Logger.create(in: core) + + // when + logger.info("message 0") + + // then + let logMatchers = try core.waitAndReturnLogMatchers() + XCTAssertEqual(logMatchers.count, 1) + + logMatchers.forEach { + $0.assertValue( + forKeyPath: "session_id", + isTypeOf: String.self + ) + } + } + + func testGivenBundlingWithRUMEnabledAndRUMFeatureEnabled_whenSendingLog_itContainsCurrentRUMContext() throws { + core.context = .mockAny() + + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + let applicationID: String = .mockRandom() + RUM.enable( + with: .init(applicationID: applicationID, sessionSampleRate: 100), + in: core + ) + + // given + let logger = Logger.create(in: core) + + // when + RUMMonitor.shared(in: core).startView(viewController: mockView) + logger.info("message 0") + RUMMonitor.shared(in: core).startAction(type: .tap, name: .mockAny()) + logger.info("message 1") + + // then + let logMatchers = try core.waitAndReturnLogMatchers() + XCTAssertEqual(logMatchers.count, 2) + + logMatchers.forEach { + $0.assertValue( + forKeyPath: "application_id", + equals: applicationID + ) + + $0.assertValue( + forKeyPath: "session_id", + isTypeOf: String.self + ) + + $0.assertValue( + forKeyPath: "view.id", + isTypeOf: String.self + ) + } + + logMatchers.first?.assertNoValue(forKeyPath: "user_action.id") + + logMatchers.last?.assertValue( + forKeyPath: "user_action.id", + isTypeOf: String.self + ) + } + + func testWhenSendingErrorOrCriticalLogs_itCreatesRUMErrorForCurrentView() throws { + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + // given + let logger = Logger.create(in: core) + RUM.enable(with: .mockAny(), in: core) + RUMMonitor.shared(in: core).startView(viewController: mockView) + + // when + logger.debug("debug message") + logger.info("info message") + logger.notice("notice message") + logger.warn("warn message") + logger.error("error message") + logger.critical("critical message") + + // then + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + let rumErrorMatcher1 = rumEventMatchers.first { $0.model(isTypeOf: RUMErrorEvent.self) } + let rumErrorMatcher2 = rumEventMatchers.last { $0.model(isTypeOf: RUMErrorEvent.self) } + try XCTUnwrap(rumErrorMatcher1).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "error message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + } + try XCTUnwrap(rumErrorMatcher2).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "critical message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + } + } + + func testWhenSendingErrorOrCriticalLogsWithAttributes_itCreatesRUMErrorForCurrentViewWithAttributes() throws { + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + // given + let logger = Logger.create(in: core) + RUM.enable(with: .mockAny(), in: core) + RUMMonitor.shared(in: core).startView(viewController: mockView) + + // when + let attributeValueA: String = .mockRandom() + logger.error("error message", attributes: [ + "any_attribute_a": attributeValueA + ]) + let attributeValueB: String = .mockRandom() + logger.critical("critical message", attributes: [ + "any_attribute_b": attributeValueB + ]) + + // then + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + let rumErrorMatcher1 = rumEventMatchers.first { $0.model(isTypeOf: RUMErrorEvent.self) } + let rumErrorMatcher2 = rumEventMatchers.last { $0.model(isTypeOf: RUMErrorEvent.self) } + try XCTUnwrap(rumErrorMatcher1).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "error message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + let attributeValue = (rumModel.context?.contextInfo["any_attribute_a"] as? AnyCodable)?.value as? String + XCTAssertEqual(attributeValue, attributeValueA) + } + try XCTUnwrap(rumErrorMatcher2).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "critical message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + let attributeValue = (rumModel.context?.contextInfo["any_attribute_b"] as? AnyCodable)?.value as? String + XCTAssertEqual(attributeValue, attributeValueB) + } + } + + func testWhenSendingErrorOrCriticalLogs_itCreatesRUMErrorWithProperSourceType() throws { + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + // given + let logger = Logger.create(in: core) + RUM.enable(with: .mockAny(), in: core) + RUMMonitor.shared(in: core).startView(viewController: mockView) + + // when + logger.error("error message", attributes: [ + "_dd.error.source_type": "flutter" + ]) + logger.critical("critical message", attributes: [ + "_dd.error.source_type": "react-native" + ]) + + // then + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + let rumErrorMatcher1 = rumEventMatchers.first { $0.model(isTypeOf: RUMErrorEvent.self) } + let rumErrorMatcher2 = rumEventMatchers.last { $0.model(isTypeOf: RUMErrorEvent.self) } + try XCTUnwrap(rumErrorMatcher1).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "error message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + XCTAssertEqual(rumModel.error.sourceType, .flutter) + } + try XCTUnwrap(rumErrorMatcher2).model(ofType: RUMErrorEvent.self) { rumModel in + XCTAssertEqual(rumModel.error.message, "critical message") + XCTAssertEqual(rumModel.error.source, .logger) + XCTAssertNil(rumModel.error.stack) + XCTAssertEqual(rumModel.error.sourceType, .reactNative) + } + } + + func testWhenSendingErrorOrCriticalLogs_itCreatesRUMErrorWithProperIsCrash() throws { + let logging: LogsFeature = .mockAny() + try core.register(feature: logging) + + // given + let logger = Logger.create(in: core) + RUM.enable(with: .mockAny(), in: core) + RUMMonitor.shared(in: core).startView(viewController: mockView) + + // when + logger.error("error message", attributes: [ + "_dd.error.is_crash": false + ]) + logger.critical("critical message", attributes: [ + "_dd.error.is_crash": true + ]) + + // then + let errorEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self) + let error1 = try XCTUnwrap(errorEvents.first) + XCTAssertEqual(error1.error.message, "error message") + XCTAssertEqual(error1.error.source, .logger) + XCTAssertNil(error1.error.stack) + // swiftlint:disable:next xct_specific_matcher + XCTAssertEqual(error1.error.isCrash, false) + + let error2 = try XCTUnwrap(errorEvents.last) + XCTAssertEqual(error2.error.message, "critical message") + XCTAssertEqual(error2.error.source, .logger) + XCTAssertNil(error2.error.stack) + // swiftlint:disable:next xct_specific_matcher + XCTAssertEqual(error2.error.isCrash, true) + } + + // MARK: - Integration With Active Span + + func testGivenBundlingWithTraceEnabledAndTracerRegistered_whenSendingLog_itContainsActiveSpanAttributes() throws { + core.context = .mockAny() + + Logs.enable(in: core) + Trace.enable(in: core) + + // given + let logger = Logger.create(in: core) + let tracer = Tracer.shared(in: core) + + // when + let span = tracer.startSpan(operationName: "span").setActive() + logger.info("info message 1") + span.finish() + logger.info("info message 2") + + // then + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue( + forKeyPath: "dd.trace_id", + equals: span.context.dd.traceID.toString(representation: .hexadecimal) + ) + logMatchers[0].assertValue( + forKeyPath: "dd.span_id", + equals: span.context.dd.spanID.toString(representation: .decimal) + ) + logMatchers[1].assertNoValue(forKey: "dd.trace_id") + logMatchers[1].assertNoValue(forKey: "dd.span_id") + } + + func testGivenBundlingWithTraceEnabledAndOpenTelemetryTracerRegistered_whenSendingLog_itContainsActiveSpanAttributes() throws { + core.context = .mockAny() + + Logs.enable(in: core) + Trace.enable(in: core) + + // given + let logger = Logger.create(in: core) + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + // when + let span = tracer.spanBuilder(spanName: "span") + .setActive(true) + .startSpan() + logger.info("info message 1") + span.end() + logger.info("info message 2") + + // then + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertValue( + forKeyPath: "dd.trace_id", + equals: span.context.traceId.toDatadog().toString(representation: .hexadecimal) + ) + logMatchers[0].assertValue( + forKeyPath: "dd.span_id", + equals: span.context.spanId.toDatadog().toString(representation: .decimal) + ) + logMatchers[1].assertNoValue(forKey: "dd.trace_id") + logMatchers[1].assertNoValue(forKey: "dd.span_id") + } + + // MARK: - Log Dates Correction + + func testGivenTimeDifferenceBetweenDeviceAndServer_whenCollectingLogs_thenLogDateUsesServerTime() throws { + // Given + let deviceTime: Date = .mockDecember15th2019At10AMUTC() + let serverTimeOffset = TimeInterval.random(in: -5..<5).rounded() // few seconds difference + + core.context = .mockWith( + serverTimeOffset: serverTimeOffset + ) + + // When + let feature: LogsFeature = .mockWith(dateProvider: RelativeDateProvider(using: deviceTime)) + try core.register(feature: feature) + + let logger = Logger.create(in: core) + logger.debug("message") + + // Then + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertDate { logDate in + logDate == deviceTime.addingTimeInterval(serverTimeOffset) + } + } + + // MARK: - Thread safety + + func testRandomlyCallingDifferentAPIsConcurrentlyDoesNotCrash() throws { + let feature: LogsFeature = .mockAny() + try core.register(feature: feature) + + let logger = Logger.create(in: core) + + DispatchQueue.concurrentPerform(iterations: 900) { iteration in + let modulo = iteration % 3 + + switch modulo { + case 0: + logger.debug("message") + logger.debug("message", attributes: ["attribute": "value"]) + case 1: + logger.addAttribute(forKey: "att\(modulo)", value: "value") + logger.addTag(withKey: "t\(modulo)", value: "value") + case 2: + logger.removeAttribute(forKey: "att\(modulo)") + logger.removeTag(withKey: "att\(modulo)") + default: + break + } + } + } + + // MARK: - Usage + + func testGivenDatadogNotInitialized_whenInitializingLogger_itPrintsError() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + // given + let core = NOPDatadogCore() + + // when + let logger = Logger.create(in: core) + + // then + XCTAssertEqual( + dd.logger.criticalLog?.message, + "Failed to build `Logger`." + ) + XCTAssertEqual( + dd.logger.criticalLog?.error?.message, + "🔥 Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.create()`." + ) + XCTAssertTrue(logger is NOPLogger) + } + + func testGivenLoggingFeatureDisabled_whenInitializingLogger_itPrintsError() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + // given + core.context = .mockAny() + XCTAssertNil(core.get(feature: LogsFeature.self)) + + // when + let logger = Logger.create(in: core) + + // then + XCTAssertEqual( + dd.logger.criticalLog?.message, + "Failed to build `Logger`." + ) + XCTAssertEqual( + dd.logger.criticalLog?.error?.message, + "🔥 Datadog SDK usage error: `Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." + ) + XCTAssertTrue(logger is NOPLogger) + } +} +// swiftlint:enable multiline_arguments_brackets diff --git a/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift b/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift new file mode 100644 index 0000000000..74294fb187 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift @@ -0,0 +1,327 @@ +/* + * 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 + +@testable import DatadogLogs +@testable import DatadogCore +@testable import DatadogCrashReporting + +class CrashLogReceiverTests: XCTestCase { + func testReceiveCrashLog() throws { + // Given + let core = PassthroughCoreMock( + bypassConsentExpectation: expectation(description: "Send Event Bypass Consent"), + messageReceiver: CrashLogReceiver.mockAny() + ) + + // When + core.send( + message: .baggage( + key: MessageBusSender.MessageKeys.crash, + value: MessageBusSender.Crash( + report: DDCrashReport.mockAny(), + context: CrashContext.mockWith(lastRUMViewEvent: nil) + ) + ) + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertEqual(core.events(ofType: LogEvent.self).count, 1, "It should send log event") + } + + // MARK: - Testing Conditional Uploads + + func testWhenCrashReportHasUnauthorizedTrackingConsent_itIsNotSent() { + // Given + let crashReport: DDCrashReport = .mockWith(date: .mockDecember15th2019At10AMUTC()) + let crashContext: CrashContext = .mockWith( + trackingConsent: [.pending, .notGranted].randomElement()! + ) + + // When + let core = PassthroughCoreMock( + messageReceiver: CrashLogReceiver.mockWith( + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()) + ) + ) + let sender = MessageBusSender(core: core) + sender.send(report: crashReport, with: crashContext) + + // Then + XCTAssertTrue(core.events(ofType: LogEvent.self).isEmpty) + } + + // MARK: - Testing Uploaded Data + + private let crashReport: DDCrashReport = .mockWith( + date: .mockDecember15th2019At10AMUTC(), + type: .mockRandom(), + message: .mockRandom(), + stack: .mockRandom(), + threads: [ + .init(name: "Thread 0", stack: "thread 0 stack", crashed: true, state: nil), + .init(name: "Thread 1", stack: "thread 1 stack", crashed: false, state: nil), + .init(name: "Thread 2", stack: "thread 2 stack", crashed: false, state: nil), + ], + binaryImages: [ + .init(libraryName: "library1", uuid: "uuid1", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad1", maxAddress: "0xMax1"), + .init(libraryName: "library2", uuid: "uuid2", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad2", maxAddress: "0xMax2"), + .init(libraryName: "library3", uuid: "uuid3", architecture: "arch", isSystemLibrary: false, loadAddress: "0xLoad3", maxAddress: "0xMax3"), + ], + meta: .init( + incidentIdentifier: "incident-identifier", + process: "process [1]", + parentProcess: "parent-process [0]", + path: "process/path", + codeType: "arch", + exceptionType: "EXCEPTION_TYPE", + exceptionCodes: "EXCEPTION_CODES" + ), + wasTruncated: false + ) + + private func crashContextWith(lastRUMViewEvent: AnyCodable?) -> CrashContext { + return .mockWith( + serverTimeOffset: .mockRandom(), + service: .mockRandom(), + env: .mockRandom(), + version: .mockRandom(), + buildNumber: .mockRandom(), + device: .mockWith( + osName: .mockRandom(), + osVersion: .mockRandom(), + osBuildNumber: .mockRandom(), + architecture: .mockRandom() + ), + sdkVersion: .mockRandom(), + userInfo: Bool.random() ? .mockRandom() : .empty, + networkConnectionInfo: .mockRandom(), + carrierInfo: .mockRandom(), + lastRUMViewEvent: lastRUMViewEvent + ) + } + + private func crashContextWith(lastLogAttributes: AnyCodable?) -> CrashContext { + return .mockWith( + serverTimeOffset: .mockRandom(), + service: .mockRandom(), + env: .mockRandom(), + version: .mockRandom(), + buildNumber: .mockRandom(), + device: .mockWith( + osName: .mockRandom(), + osVersion: .mockRandom(), + osBuildNumber: .mockRandom(), + architecture: .mockRandom() + ), + sdkVersion: .mockRandom(), + userInfo: Bool.random() ? .mockRandom() : .empty, + networkConnectionInfo: .mockRandom(), + carrierInfo: .mockRandom(), + lastLogAttributes: lastLogAttributes + ) + } + + func testWhenSendingCrashReport_itEncodesErrorInformation() throws { + // Given (CR with no link to RUM view) + let crashContext = crashContextWith(lastRUMViewEvent: nil) // no RUM view information + + // When + let core = PassthroughCoreMock( + messageReceiver: CrashLogReceiver.mockWith( + dateProvider: RelativeDateProvider(using: .mockRandomInThePast()) + ) + ) + + let sender = MessageBusSender(core: core) + sender.send(report: crashReport, with: crashContext) + + // Then + let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first) + let user = try XCTUnwrap(crashContext.userInfo) + + let expectedLog = LogEvent( + date: crashReport.date!.addingTimeInterval(crashContext.serverTimeOffset), + status: .emergency, + message: crashReport.message, + error: .init( + kind: crashReport.type, + message: crashReport.message, + stack: crashReport.stack, + sourceType: "ios" + ), + serviceName: crashContext.service, + environment: crashContext.env, + loggerName: "crash-reporter", + loggerVersion: crashContext.sdkVersion, + threadName: nil, + applicationVersion: crashContext.version, + applicationBuildNumber: crashContext.buildNumber, + buildId: nil, + variant: core.context.variant, + dd: .init( + device: .init( + brand: crashContext.device.brand, + name: crashContext.device.name, + model: crashContext.device.model, + architecture: crashContext.device.architecture + ) + ), + os: .init( + name: crashContext.device.osName, + version: crashContext.device.osVersion, + build: crashContext.device.osBuildNumber + ), + userInfo: .init( + id: user.id, + name: user.name, + email: user.email, + extraInfo: user.extraInfo + ), + networkConnectionInfo: crashContext.networkConnectionInfo, + mobileCarrierInfo: crashContext.carrierInfo, + attributes: .init( + userAttributes: [:], + internalAttributes: [ + DDError.threads: crashReport.threads, + DDError.binaryImages: crashReport.binaryImages, + DDError.meta: crashReport.meta, + DDError.wasTruncated: false + ] + ), + tags: nil + ) + + DDAssertJSONEqual(expectedLog, log) + } + + // swiftlint:disable multiline_literal_brackets + func testWhenSendingCrashReportWithRUMContext_itEncodesErrorInformation() throws { + // Given (CR with the link to RUM view) + let crashContext = crashContextWith( + lastRUMViewEvent: AnyCodable( + [ // partial RUM view information, necessary for the link + "application": ["id": "rum-app-id"], + "session": ["id": "rum-session-id"], + "view": ["id": "rum-view-id"], + ] + ) + ) + + // When + let core = PassthroughCoreMock( + messageReceiver: CrashLogReceiver(dateProvider: SystemDateProvider(), logEventMapper: nil) + ) + + let sender = MessageBusSender(core: core) + sender.send(report: crashReport, with: crashContext) + + // Then + let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first) + + XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.applicationID] as? String, "rum-app-id") + XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.sessionID] as? String, "rum-session-id") + XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.viewID] as? String, "rum-view-id") + XCTAssertNil(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.actionID]) + } + // swiftlint:enable multiline_literal_brackets + + func testWhenSendingCrashReportWithSourceType_itEncodesSourceType() throws { + // Given (CR with the link to RUM view) + let crashContext = crashContextWith(lastRUMViewEvent: nil) + + // When + let core = PassthroughCoreMock( + context: .mockWith(nativeSourceOverride: "ios+il2cpp"), + messageReceiver: CrashLogReceiver(dateProvider: SystemDateProvider(), logEventMapper: nil) + ) + + let sender = MessageBusSender(core: core) + sender.send(report: crashReport, with: crashContext) + + // Then + let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first) + + XCTAssertEqual(log.error?.sourceType, "ios+il2cpp") + } + + func testWhenSendingCrashReportWithMalformedRUMContext_itSendsErrorTelemetry() throws { + // Given (CR with the link to RUM view) + let crashContext = crashContextWith( + lastRUMViewEvent: AnyCodable(["rum-view": "malformed"]) + ) + + // When + let telemetry = TelemetryReceiverMock() + let core = PassthroughCoreMock( + messageReceiver: CombinedFeatureMessageReceiver([ + CrashLogReceiver(dateProvider: SystemDateProvider(), logEventMapper: nil), + telemetry + ]) + ) + + let sender = MessageBusSender(core: core) + sender.send(report: crashReport, with: crashContext) + + // Then + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertTrue(error.message.hasPrefix("Failed to decode crash message in `LogMessageReceiver`")) + XCTAssertTrue(core.events(ofType: LogEvent.self).isEmpty, "It should send no log") + } + + func testWhenSendingCrashContextWithLogAttributes_itSendsThemToLog() throws { + // Given + let stringAttribute: String = .mockRandom() + let boolAttribute: Bool = .mockRandom() + let crashContext = crashContextWith(lastLogAttributes: .init( + [ + "mock-string-attribute": stringAttribute, + "mock-bool-attribute": boolAttribute + ] as [String: Any] + )) + let core = PassthroughCoreMock( + messageReceiver: CrashLogReceiver(dateProvider: SystemDateProvider(), logEventMapper: nil) + ) + let sender = MessageBusSender(core: core) + + // When + sender.send(report: crashReport, with: crashContext) + + // Then + let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first) + + XCTAssertEqual((log.attributes.userAttributes["mock-string-attribute"] as? AnyCodable)?.value as? String, stringAttribute) + XCTAssertEqual((log.attributes.userAttributes["mock-bool-attribute"] as? AnyCodable)?.value as? Bool, boolAttribute) + } + + func testWhenSendingCrashWithLogMapper_itSendsModifiedCrash() throws { + // Given + let errorFingerprint: String = .mockRandom() + let core = PassthroughCoreMock( + messageReceiver: CrashLogReceiver( + dateProvider: SystemDateProvider(), + logEventMapper: SyncLogEventMapper({ event in + var event = event + event.error?.fingerprint = errorFingerprint + return event + }) + ) + ) + let sender = MessageBusSender(core: core) + + // When + sender.send(report: crashReport, with: .mockAny()) + + // Then + let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first) + + XCTAssertEqual(log.error?.fingerprint, errorFingerprint) + } +} diff --git a/DatadogCore/Tests/Datadog/Logs/DatadogLogsFeatureTests.swift b/DatadogCore/Tests/Datadog/Logs/DatadogLogsFeatureTests.swift new file mode 100644 index 0000000000..0f55031a29 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Logs/DatadogLogsFeatureTests.swift @@ -0,0 +1,160 @@ +/* + * 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 + +@testable import DatadogLogs +@testable import DatadogCore + +class DatadogLogsFeatureTests: XCTestCase { + override func setUp() { + super.setUp() + temporaryCoreDirectory.create() + } + + override func tearDown() { + temporaryCoreDirectory.delete() + super.tearDown() + } + + // MARK: - HTTP Message + + func testItUsesExpectedHTTPMessage() throws { + let randomApplicationName: String = .mockRandom(among: .alphanumerics) + let randomApplicationVersion: String = .mockRandom() + let randomSource: String = .mockRandom(among: .alphanumerics) + let randomOrigin: String = .mockRandom(among: .alphanumerics) + let randomSDKVersion: String = .mockRandom(among: .alphanumerics) + let randomUploadURL: URL = .mockRandom() + let randomClientToken: String = .mockRandom() + let randomDeviceName: String = .mockRandom() + let randomDeviceOSName: String = .mockRandom() + let randomDeviceOSVersion: String = .mockRandom() + let randomEncryption: DataEncryption? = Bool.random() ? DataEncryptionMock() : nil + let randomBackgroundTasksEnabled: Bool = .mockRandom() + + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let httpClient = URLSessionClient(session: server.getInterceptedURLSession()) + + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .combining( + storagePerformance: .writeEachObjectToNewFileAndReadAllFiles, + uploadPerformance: .veryQuick + ), + httpClient: httpClient, + encryption: randomEncryption, + contextProvider: .mockWith( + context: .mockWith( + clientToken: randomClientToken, + version: randomApplicationVersion, + source: randomSource, + sdkVersion: randomSDKVersion, + ciAppOrigin: randomOrigin, + applicationName: randomApplicationName, + device: .mockWith( + name: randomDeviceName, + osName: randomDeviceOSName, + osVersion: randomDeviceOSVersion + ) + ) + ), + applicationVersion: randomApplicationVersion, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: randomBackgroundTasksEnabled + ) + defer { core.flushAndTearDown() } + + // Given + Logs.enable(with: .init(customEndpoint: randomUploadURL), in: core) + + // When + let logger = Logger.create(in: core) + logger.debug(.mockAny()) + + // Then + let request = server.waitAndReturnRequests(count: 1)[0] + let requestURL = try XCTUnwrap(request.url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(requestURL.absoluteString.starts(with: randomUploadURL.absoluteString + "?")) + XCTAssertEqual(requestURL.query, "ddsource=\(randomSource)") + XCTAssertEqual( + request.allHTTPHeaderFields?["User-Agent"], + """ + \(randomApplicationName)/\(randomApplicationVersion) CFNetwork (\(randomDeviceName); \(randomDeviceOSName)/\(randomDeviceOSVersion)) + """ + ) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Encoding"], "deflate") + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomOrigin) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], randomSDKVersion) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-REQUEST-ID"]?.matches(regex: .uuidRegex), true) + } + + // MARK: - HTTP Payload + + func testItUsesExpectedPayloadFormatForUploads() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let httpClient = URLSessionClient(session: server.getInterceptedURLSession()) + + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .combining( + storagePerformance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write all events to single file, + minFileAgeForRead: StoragePerformanceMock.readAllFiles.minFileAgeForRead, + maxFileAgeForRead: StoragePerformanceMock.readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 3, // write 3 spans to payload, + maxObjectSize: .max + ), + uploadPerformance: UploadPerformanceMock( + initialUploadDelay: 0.5, // wait enough until events are written, + minUploadDelay: 1, + maxUploadDelay: 1, + uploadDelayChangeRate: 0 + ) + ), + httpClient: httpClient, + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + // Given + Logs.enable(with: .init(), in: core) + + let logger = Logger.create(in: core) + logger.debug("log 1") + logger.debug("log 2") + logger.debug("log 3") + + let payload = try XCTUnwrap(server.waitAndReturnRequests(count: 1)[0].httpBody) + + // Expected payload format: + // `[log1JSON,log2JSON,log3JSON]` + + XCTAssertEqual(payload.prefix(1).utf8String, "[", "payload should start with JSON array trait: `[`") + XCTAssertEqual(payload.suffix(1).utf8String, "]", "payload should end with JSON array trait: `]`") + + // Expect payload to be an array of log JSON objects + let logMatchers = try LogMatcher.fromArrayOfJSONObjectsData(payload) + logMatchers[0].assertMessage(equals: "log 1") + logMatchers[1].assertMessage(equals: "log 2") + logMatchers[2].assertMessage(equals: "log 3") + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift new file mode 100644 index 0000000000..d374ccb3e9 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/CoreMocks.swift @@ -0,0 +1,381 @@ +/* + * 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 + +@testable import DatadogLogs +@testable import DatadogCore + +// MARK: - Configuration Mocks + +extension Datadog.Configuration { + static func mockAny() -> Datadog.Configuration { .mockWith() } + + static func mockWith( + clientToken: String = .mockAny(), + env: String = .mockAny(), + site: DatadogSite = .us1, + service: String? = .mockAny(), + bundle: Bundle = .main, + batchSize: BatchSize = .medium, + uploadFrequency: UploadFrequency = .average, + proxyConfiguration: [AnyHashable: Any]? = nil, + encryption: DataEncryption? = nil, + serverDateProvider: ServerDateProvider? = nil + ) -> Self { + .init( + clientToken: clientToken, + env: env, + site: site, + service: service, + bundle: bundle, + batchSize: batchSize, + uploadFrequency: uploadFrequency, + proxyConfiguration: proxyConfiguration, + encryption: encryption, + serverDateProvider: serverDateProvider + ) + } +} + +typealias BatchSize = Datadog.Configuration.BatchSize + +extension BatchSize: CaseIterable { + public static var allCases: [Self] { [.small, .medium, .large] } + + static func mockRandom() -> Self { + allCases.randomElement()! + } +} + +typealias UploadFrequency = Datadog.Configuration.UploadFrequency + +extension UploadFrequency: CaseIterable { + public static var allCases: [Self] { [.frequent, .average, .rare] } + + static func mockRandom() -> Self { + allCases.randomElement()! + } +} + +extension BundleType: CaseIterable { + public static var allCases: [Self] { [.iOSApp, iOSAppExtension] } +} + +struct DataEncryptionMock: DataEncryption { + let enc: (Data) throws -> Data + let dec: (Data) throws -> Data + + init( + encrypt: @escaping (Data) throws -> Data = { $0 }, + decrypt: @escaping (Data) throws -> Data = { $0 } + ) { + enc = encrypt + dec = decrypt + } + + func encrypt(data: Data) throws -> Data { try enc(data) } + func decrypt(data: Data) throws -> Data { try dec(data) } +} + +class ServerDateProviderMock: ServerDateProvider { + private var update: (TimeInterval) -> Void = { _ in } + + var offset: TimeInterval = .zero { + didSet { update(offset) } + } + + func synchronize(update: @escaping (TimeInterval) -> Void) { + self.update = update + } +} + +// MARK: - PerformancePreset Mocks + +struct StoragePerformanceMock: StoragePerformancePreset { + var maxFileSize: UInt64 + var maxDirectorySize: UInt64 + var maxFileAgeForWrite: TimeInterval + var minFileAgeForRead: TimeInterval + var maxFileAgeForRead: TimeInterval + var maxObjectsInFile: Int + var maxObjectSize: UInt64 + + static let noOp = StoragePerformanceMock( + maxFileSize: 0, + maxDirectorySize: 0, + maxFileAgeForWrite: 0, + minFileAgeForRead: 0, + maxFileAgeForRead: 0, + maxObjectsInFile: 0, + maxObjectSize: 0 + ) + + static let readAllFiles = StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: 0, + minFileAgeForRead: -1, // make all files eligible for read + maxFileAgeForRead: .distantFuture, // make all files eligible for read + maxObjectsInFile: .max, + maxObjectSize: .max + ) + + static let writeEachObjectToNewFileAndReadAllFiles = StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: 0, // always return new file for writing + minFileAgeForRead: readAllFiles.minFileAgeForRead, + maxFileAgeForRead: readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 1, // write each data to new file + maxObjectSize: .max + ) +} + +extension StoragePerformanceMock { + init(other: StoragePerformancePreset) { + maxFileSize = other.maxFileSize + maxDirectorySize = other.maxDirectorySize + maxFileAgeForWrite = other.maxFileAgeForWrite + minFileAgeForRead = other.minFileAgeForRead + maxFileAgeForRead = other.maxFileAgeForRead + maxObjectsInFile = other.maxObjectsInFile + maxObjectSize = other.maxObjectSize + } +} + +struct UploadPerformanceMock: UploadPerformancePreset { + var initialUploadDelay: TimeInterval + var minUploadDelay: TimeInterval + var maxUploadDelay: TimeInterval + var uploadDelayChangeRate: Double + + static let noOp = UploadPerformanceMock( + initialUploadDelay: .distantFuture, + minUploadDelay: .distantFuture, + maxUploadDelay: .distantFuture, + uploadDelayChangeRate: 0 + ) + + /// Optimized for performing very fast uploads in unit tests. + static let veryQuick = UploadPerformanceMock( + initialUploadDelay: 0.05, + minUploadDelay: 0.05, + maxUploadDelay: 0.05, + uploadDelayChangeRate: 0 + ) + + /// Optimized for performing very fast first upload and then changing to unrealistically long intervals. + static let veryQuickInitialUpload = UploadPerformanceMock( + initialUploadDelay: 0.05, + minUploadDelay: 60, + maxUploadDelay: 60, + uploadDelayChangeRate: 60 / 0.05 + ) +} + +extension UploadPerformanceMock { + init(other: UploadPerformancePreset) { + initialUploadDelay = other.initialUploadDelay + minUploadDelay = other.minUploadDelay + maxUploadDelay = other.maxUploadDelay + uploadDelayChangeRate = other.uploadDelayChangeRate + } +} + +extension PerformancePreset: AnyMockable, RandomMockable { + public static func mockAny() -> Self { + PerformancePreset(batchSize: .medium, uploadFrequency: .average, bundleType: .iOSApp) + } + + public static func mockRandom() -> Self { + PerformancePreset(batchSize: .mockRandom(), uploadFrequency: .mockRandom(), bundleType: .mockRandom()) + } + + static func combining(storagePerformance storage: StoragePerformanceMock, uploadPerformance upload: UploadPerformanceMock) -> Self { + PerformancePreset( + maxFileSize: storage.maxFileSize, + maxDirectorySize: storage.maxDirectorySize, + maxFileAgeForWrite: storage.maxFileAgeForWrite, + minFileAgeForRead: storage.minFileAgeForRead, + maxFileAgeForRead: storage.maxFileAgeForRead, + maxObjectsInFile: storage.maxObjectsInFile, + maxObjectSize: storage.maxObjectSize, + initialUploadDelay: upload.initialUploadDelay, + minUploadDelay: upload.minUploadDelay, + maxUploadDelay: upload.maxUploadDelay, + uploadDelayChangeRate: upload.uploadDelayChangeRate + ) + } +} + +extension FeatureStorage { + static func mockNoOp() -> FeatureStorage { + return FeatureStorage( + featureName: .mockAny(), + queue: DispatchQueue(label: "nop"), + directories: temporaryFeatureDirectories, + authorizedFilesOrchestrator: NOPFilesOrchestrator(), + unauthorizedFilesOrchestrator: NOPFilesOrchestrator(), + encryption: nil, + telemetry: NOPTelemetry() + ) + } +} + +extension FeatureUpload { + static func mockNoOp() -> FeatureUpload { + return FeatureUpload(uploader: NOPDataUploadWorker()) + } +} + +extension Reader { + func markBatchAsRead(_ batch: Batch) { + // We can ignore `reason` in most tests (used for sending metric), so we provide this convenience variant. + markBatchAsRead(batch, reason: .flushed) + } +} + +extension FilesOrchestratorType { + func delete(readableFile: ReadableFile) { + // We can ignore `deletionReason` in most tests (used for sending metric), so we provide this convenience variant. + delete(readableFile: readableFile, deletionReason: .flushed) + } +} + +class NOPReader: Reader { + func readFiles(limit: Int) -> [ReadableFile] { [] } + func readBatch(from file: ReadableFile) -> Batch? { nil } + func markBatchAsRead(_ batch: Batch, reason: BatchDeletedMetric.RemovalReason) {} +} + +internal class NOPFilesOrchestrator: FilesOrchestratorType { + struct NOPFile: WritableFile, ReadableFile { + var name: String = .mockAny() + func size() throws -> UInt64 { .mockAny() } + func append(data: Data) throws {} + func stream() throws -> InputStream { InputStream() } + func delete() throws { } + } + + var performance: StoragePerformancePreset { StoragePerformanceMock.noOp } + + func getWritableFile(writeSize: UInt64) throws -> WritableFile { NOPFile() } + func getReadableFiles(excludingFilesNamed excludedFileNames: Set, limit: Int) -> [ReadableFile] { [] } + func delete(readableFile: ReadableFile, deletionReason: BatchDeletedMetric.RemovalReason) { } + + var ignoreFilesAgeWhenReading = false + var trackName: String = "nop" +} + +extension DataFormat { + static func mockAny() -> DataFormat { + return mockWith() + } + + static func mockWith( + prefix: String = .mockAny(), + suffix: String = .mockAny(), + separator: Character = .mockAny() + ) -> DataFormat { + return DataFormat( + prefix: prefix, + suffix: suffix, + separator: separator + ) + } +} + +class NOPDataUploadWorker: DataUploadWorkerType { + func flushSynchronously() {} + func cancelSynchronously() {} +} + +internal class DataUploaderMock: DataUploaderType { + let uploadStatuses: [DataUploadStatus] + + /// Notifies on each started upload. + var onUpload: ((DataUploadStatus?) throws -> Void)? + + /// Tracks uploaded events. + private(set) var uploadedEvents: [Event] = [] + + 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: [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)] + } +} + +extension DataUploadStatus: RandomMockable { + public static func mockRandom() -> DataUploadStatus { + return DataUploadStatus( + needsRetry: .random(), + responseCode: .mockRandom(), + userDebugDescription: .mockRandom(), + error: nil, + attempt: .mockRandom() + ) + } + + static func mockWith( + needsRetry: Bool = .mockAny(), + responseCode: Int = .mockAny(), + userDebugDescription: String = .mockAny(), + error: DataUploadError? = nil, + attempt: UInt = 0 + ) -> DataUploadStatus { + return DataUploadStatus( + needsRetry: needsRetry, + responseCode: responseCode, + userDebugDescription: userDebugDescription, + error: error, + attempt: attempt + ) + } +} + +extension BatteryStatus.State { + static func mockRandom(within cases: [BatteryStatus.State] = [.unknown, .unplugged, .charging, .full]) -> BatteryStatus.State { + return cases.randomElement()! + } +} + +class MockHostsSanitizer: HostsSanitizing { + private(set) var sanitizations = [(hosts: Set, warningMessage: String)]() + func sanitized(hosts: Set, warningMessage: String) -> Set { + sanitizations.append((hosts: hosts, warningMessage: warningMessage)) + return hosts + } + + func sanitized( + hostsWithTracingHeaderTypes: [String: Set], + warningMessage: String + ) -> [String: Set] { + sanitizations.append((hosts: Set(hostsWithTracingHeaderTypes.keys), warningMessage: warningMessage)) + return hostsWithTracingHeaderTypes + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift new file mode 100644 index 0000000000..58a2bee8fa --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -0,0 +1,265 @@ +/* + * 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 TestUtilities +import DatadogInternal + +@testable import DatadogLogs +@testable import DatadogRUM +@testable import DatadogCrashReporting +@testable import DatadogCore + +extension CrashReportingFeature { + /// Mocks the Crash Reporting feature instance which doesn't load crash reports. + static func mockNoOp( + core: DatadogCoreProtocol = NOPDatadogCore(), + crashReportingPlugin: CrashReportingPlugin = NOPCrashReportingPlugin() + ) -> Self { + return .mockWith( + integration: MessageBusSender(core: core), + crashReportingPlugin: crashReportingPlugin + ) + } + + static func mockWith( + integration: CrashReportSender, + crashReportingPlugin: CrashReportingPlugin = NOPCrashReportingPlugin(), + crashContextProvider: CrashContextProvider = CrashContextProviderMock(), + messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), + telemetry: Telemetry = NOPTelemetry() + ) -> Self { + .init( + crashReportingPlugin: crashReportingPlugin, + crashContextProvider: crashContextProvider, + sender: integration, + messageReceiver: messageReceiver, + telemetry: telemetry + ) + } +} + +internal class CrashReportingPluginMock: CrashReportingPlugin { + /// The crash report loaded by this plugin. + var pendingCrashReport: DDCrashReport? + /// If the plugin was asked to delete the crash report. + @ReadWriteLock + 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) + didReadPendingCrashReport?() + } + + /// Notifies the `readPendingCrashReport(completion:)` return. + var didReadPendingCrashReport: (() -> Void)? + + func inject(context: Data) { + injectedContextData = context + didInjectContext?() + } + + /// 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 { + private(set) var currentCrashContext: CrashContext? + var onCrashContextChange: (CrashContext) -> Void + + init(initialCrashContext: CrashContext = .mockAny()) { + self.currentCrashContext = initialCrashContext + self.onCrashContextChange = { _ in } + } +} + +class CrashReportSenderMock: CrashReportSender { + var sentCrashReport: DDCrashReport? + var sentCrashContext: CrashContext? + + func send(report: DDCrashReport, with context: CrashContext) { + sentCrashReport = report + sentCrashContext = context + didSendCrashReport?() + } + + var didSendCrashReport: (() -> Void)? + + func send(launch: DatadogInternal.LaunchReport) {} +} + +class RUMCrashReceiverMock: FeatureMessageReceiver { + var receivedBaggage: FeatureBaggage? + + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + switch message { + case .baggage(let label, let baggage) where label == CrashReportReceiver.MessageKeys.crash: + receivedBaggage = baggage + return true + default: + return false + } + } +} + +class LogsCrashReceiverMock: FeatureMessageReceiver { + var receivedBaggage: FeatureBaggage? + + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + switch message { + case .baggage(let label, let baggage) where label == LoggingMessageKeys.crash: + receivedBaggage = baggage + return true + default: + return false + } + } +} + +extension CrashContext { + static func mockAny() -> CrashContext { + return mockWith() + } + + static func mockWith( + serverTimeOffset: TimeInterval = .zero, + service: String = .mockAny(), + env: String = .mockAny(), + version: String = .mockAny(), + buildNumber: String = .mockAny(), + device: DeviceInfo = .mockAny(), + sdkVersion: String = .mockAny(), + source: String = .mockAny(), + trackingConsent: TrackingConsent = .granted, + userInfo: UserInfo? = .mockAny(), + networkConnectionInfo: NetworkConnectionInfo? = .mockAny(), + carrierInfo: CarrierInfo? = .mockAny(), + lastRUMViewEvent: AnyCodable? = nil, + lastRUMSessionState: AnyCodable? = nil, + lastIsAppInForeground: Bool = .mockAny(), + appLaunchDate: Date? = .mockRandomInThePast(), + lastRUMAttributes: GlobalRUMAttributes? = nil, + lastLogAttributes: AnyCodable? = nil + ) -> Self { + .init( + serverTimeOffset: serverTimeOffset, + service: service, + env: env, + version: version, + buildNumber: buildNumber, + device: device, + sdkVersion: service, + source: source, + trackingConsent: trackingConsent, + userInfo: userInfo, + networkConnectionInfo: networkConnectionInfo, + carrierInfo: carrierInfo, + lastIsAppInForeground: lastIsAppInForeground, + appLaunchDate: appLaunchDate, + lastRUMViewEvent: lastRUMViewEvent, + lastRUMSessionState: lastRUMSessionState, + lastRUMAttributes: lastRUMAttributes, + lastLogAttributes: lastLogAttributes + ) + } + + static func mockRandom() -> Self { + .init( + serverTimeOffset: .zero, + service: .mockRandom(), + env: .mockRandom(), + version: .mockRandom(), + buildNumber: .mockRandom(), + device: .mockRandom(), + sdkVersion: .mockRandom(), + source: .mockRandom(), + trackingConsent: .granted, + userInfo: .mockRandom(), + networkConnectionInfo: .mockRandom(), + carrierInfo: .mockRandom(), + lastIsAppInForeground: .mockRandom(), + appLaunchDate: .mockRandomInThePast(), + lastRUMViewEvent: AnyCodable(mockRandomAttributes()), + lastRUMSessionState: AnyCodable(mockRandomAttributes()), + lastRUMAttributes: GlobalRUMAttributes(attributes: mockRandomAttributes()), + lastLogAttributes: AnyCodable(mockRandomAttributes()) + ) + } + + var data: Data { try! JSONEncoder.dd.default().encode(self) } +} + +internal extension DDCrashReport { + static func mockAny() -> DDCrashReport { + return .mockWith() + } + + static func mockWith( + date: Date? = .mockAny(), + type: String = .mockAny(), + message: String = .mockAny(), + stack: String = .mockAny(), + threads: [DDThread] = [], + binaryImages: [BinaryImage] = [], + meta: Meta = .mockAny(), + wasTruncated: Bool = .mockAny(), + context: Data? = .mockAny(), + additionalAttributes: [String: Encodable]? = nil + ) -> DDCrashReport { + return DDCrashReport( + date: date, + type: type, + message: message, + stack: stack, + threads: threads, + binaryImages: binaryImages, + meta: meta, + wasTruncated: wasTruncated, + context: context, + additionalAttributes: additionalAttributes + ) + } + + static func mockRandomWith(context: CrashContext) -> DDCrashReport { + return mockRandomWith(contextData: context.data) + } + + static func mockRandomWith(contextData: Data) -> DDCrashReport { + return mockWith( + date: .mockRandomInThePast(), + type: .mockRandom(), + message: .mockRandom(), + stack: .mockRandom(), + context: contextData, + additionalAttributes: mockRandomAttributes() + ) + } +} + +internal extension DDCrashReport.Meta { + static func mockAny() -> DDCrashReport.Meta { + return DDCrashReport.Meta( + incidentIdentifier: nil, + process: nil, + parentProcess: nil, + path: nil, + codeType: nil, + exceptionType: nil, + exceptionCodes: nil + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValuePublisherMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValuePublisherMock.swift new file mode 100644 index 0000000000..6a98b23c6b --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogCore/ContextValuePublisherMock.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 Foundation +@testable import DatadogCore + +internal class ContextValuePublisherMock: ContextValuePublisher { + private let queue = DispatchQueue( + label: "com.datadoghq.context-value-publisher-mock" + ) + + let initialValue: Value + + var value: Value { + get { queue.sync { _value } } + set { queue.sync { _value = newValue } } + } + + private var receiver: ContextValueReceiver? + private var _value: Value { + didSet { receiver?(_value) } + } + + init(initialValue: Value) { + self.initialValue = initialValue + self._value = initialValue + } + + init() where Value: ExpressibleByNilLiteral { + initialValue = nil + _value = nil + } + + func publish(to receiver: @escaping ContextValueReceiver) { + queue.sync { self.receiver = receiver } + } + + func cancel() { + queue.sync { receiver = nil } + } +} + +extension ContextValuePublisher { + static func mockAny() -> ContextValuePublisherMock where Value: ExpressibleByNilLiteral { + .init() + } + + static func mockWith(initialValue: Value) -> ContextValuePublisherMock { + .init(initialValue: initialValue) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogCore/DatadogContextProviderMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogCore/DatadogContextProviderMock.swift new file mode 100644 index 0000000000..dde9b91355 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogCore/DatadogContextProviderMock.swift @@ -0,0 +1,18 @@ +/* + * 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 TestUtilities +import DatadogInternal +@testable import DatadogCore + +extension DatadogContextProvider: AnyMockable { + public static func mockAny() -> Self { .mockWith() } + + static func mockWith(context: DatadogContext = .mockAny()) -> Self { + .init(context: context) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift new file mode 100644 index 0000000000..d0ecde8792 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -0,0 +1,222 @@ +/* + * 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 +@testable import DatadogCore + +/// A `DatadogCoreProtocol` which proxies all calls to the real `DatadogCore` implementation. It intercepts +/// all events written to the actual core and provides APIs to read their values back for tests. +/// +/// Usage example: +/// +/// ``` +/// let core = DatadogCoreProxy(context: .mockWith(service: "foo-bar")) +/// defer { core.flushAndTearDown() } +/// core.register(feature: LoggingFeature.mockAny()) +/// +/// let logger = Logger.builder.build(in: core) +/// logger.debug("message") +/// +/// let events = core.waitAndReturnEvents(of: LoggingFeature.self, ofType: LogEvent.self) +/// XCTAssertEqual(events[0].serviceName, "foo-bar") +/// ``` +/// +internal class DatadogCoreProxy: DatadogCoreProtocol { + /// Counts references to `DatadogCoreProxy` instances, so we can prevent memory + /// leaks of SDK core in `DatadogTestsObserver`. + static var referenceCount = 0 + + /// The SDK core managed by this proxy. + private let core: DatadogCore + + @ReadWriteLock + private var featureScopeInterceptors: [String: FeatureScopeInterceptor] = [:] + + 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) + DatadogCoreProxy.referenceCount += 1 + } + + deinit { + DatadogCoreProxy.referenceCount -= 1 + } + + var context: DatadogContext { + didSet { + core.contextProvider.replace(context: context) + } + } + + func register(feature: T) throws where T: DatadogFeature { + try core.register(feature: feature) + } + + func feature(named name: String, type: T.Type) -> T? { + return core.feature(named: name, type: type) + } + + func scope(for featureType: T.Type) -> FeatureScope where T: DatadogFeature { + if featureScopeInterceptors[T.name] == nil { + featureScopeInterceptors[T.name] = FeatureScopeInterceptor() + } + return FeatureScopeProxy( + proxy: core.scope(for: featureType), + interceptor: featureScopeInterceptors[T.name]! + ) + } + + func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { + core.set(baggage: baggage, forKey: key) + } + + 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 { + func flush() { + core.flush() + } + + func flushAndTearDown() { + core.flushAndTearDown() + + if temporaryCoreDirectory.coreDirectory.exists() { + temporaryCoreDirectory.coreDirectory.delete() + } + if temporaryCoreDirectory.osDirectory.exists() { + temporaryCoreDirectory.osDirectory.delete() + } + } +} + +private struct FeatureScopeProxy: FeatureScope { + let proxy: FeatureScope + let interceptor: FeatureScopeInterceptor + + func eventWriteContext(bypassConsent: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { + interceptor.enter() + proxy.eventWriteContext(bypassConsent: bypassConsent) { context, writer in + block(context, interceptor.intercept(writer: writer)) + interceptor.leave() + } + } + + func context(_ block: @escaping (DatadogContext) -> Void) { + interceptor.enter() + proxy.context { context in + block(context) + interceptor.leave() + } + } + + var telemetry: Telemetry { proxy.telemetry } + var dataStore: DataStore { proxy.dataStore } + + func send(message: FeatureMessage, else fallback: @escaping () -> Void) { + proxy.send(message: message, else: fallback) + } + + func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { + proxy.set(baggage: baggage, forKey: key) + } +} + +private final class FeatureScopeInterceptor: @unchecked Sendable { + struct InterceptingWriter: Writer { + static let jsonEncoder = JSONEncoder.dd.default() + + let group: DispatchGroup + let actualWriter: Writer + unowned var interception: FeatureScopeInterceptor? + + func write(value: T, metadata: M) { + group.enter() + defer { group.leave() } + + actualWriter.write(value: value, metadata: metadata) + + let event = value + let data = try! InterceptingWriter.jsonEncoder.encode(value) + interception?.events.append((event, data)) + } + } + + func intercept(writer: Writer) -> Writer { + return InterceptingWriter(group: group, actualWriter: writer, interception: self) + } + + // MARK: - Synchronizing and awaiting events: + + @ReadWriteLock + private var events: [(event: Any, data: Data)] = [] + + private let group = DispatchGroup() + + func enter() { group.enter() } + func leave() { group.leave() } + + func waitAndReturnEvents(timeout: DispatchTime) -> [(event: Any, data: Data)] { + _ = group.wait(timeout: timeout) + return events + } +} + +extension DatadogCoreProxy { + /// Returns all events of given type for certain Feature. + /// - Parameters: + /// - name: The Feature to retrieve events from + /// - type: The type of events to filter out + /// - Returns: A list of events. + func waitAndReturnEvents(ofFeature name: String, ofType type: T.Type, timeout: DispatchTime = .distantFuture) -> [T] where T: Encodable { + flush() + guard let interceptor = self.featureScopeInterceptors[name] else { + return [] // feature scope was not requested, so there's no interception + } + return interceptor.waitAndReturnEvents(timeout: timeout).compactMap { $0.event as? T } + } + + /// Returns serialized events of given Feature. + /// + /// - Parameter feature: The Feature to retrieve events from + /// - Returns: A list of serialized events. + func waitAndReturnEventsData(ofFeature name: String, timeout: DispatchTime = .distantFuture) -> [Data] { + flush() + guard let interceptor = self.featureScopeInterceptors[name] else { + return [] // feature scope was not requested, so there's no interception + } + return interceptor.waitAndReturnEvents(timeout: timeout).map { $0.data } + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogRemoteFeatureMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogRemoteFeatureMock.swift new file mode 100644 index 0000000000..fd172a51e8 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogRemoteFeatureMock.swift @@ -0,0 +1,17 @@ +/* + * 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 TestUtilities +import DatadogInternal +import DatadogCore + +internal struct DatadogRemoteFeatureMock: DatadogRemoteFeature { + static let name = "mock" + var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock() + var messageReceiver: FeatureMessageReceiver = FeatureMessageReceiverMock() + var performanceOverride: PerformancePresetOverride? = nil +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift new file mode 100644 index 0000000000..7b8f9e4f97 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/UploadMock.swift @@ -0,0 +1,111 @@ +/* + * 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 TestUtilities +import DatadogInternal + +internal class FeatureRequestBuilderMock: FeatureRequestBuilder { + private let factory: (([Event], DatadogContext) throws -> URLRequest) + + init(factory: @escaping (([Event], DatadogContext) throws -> URLRequest) = { _, _ in .mockAny() }) { + self.factory = factory + } + + convenience init(request: URLRequest) { + self.init(factory: { _, _ in request }) + } + + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { + return try factory(events, context) + } +} + +internal class FeatureRequestBuilderSpy: FeatureRequestBuilder { + /// Stores the parameters passed to the `request(for:with:)` method. + @ReadWriteLock + private(set) var requestParameters: [(events: [Event], context: DatadogContext)] = [] + + /// A closure that is called when a request is about to be created in the `request(for:with:)` method. + @ReadWriteLock + var onRequest: ((_ events: [Event], _ context: DatadogContext) -> Void)? + + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { + requestParameters.append((events: events, context: context)) + onRequest?(events, context) + return .mockAny() + } +} + +internal struct FailingRequestBuilderMock: FeatureRequestBuilder { + let error: Error + + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) throws -> URLRequest { + throw error + } +} + +extension URLRequestBuilder.QueryItem: RandomMockable, AnyMockable { + public static func mockRandom() -> Self { + let all: [URLRequestBuilder.QueryItem] = [ + .ddsource(source: .mockRandom()), + .ddtags(tags: .mockRandom()), + ] + return all.randomElement()! + } + + public static func mockAny() -> Self { + return .ddsource(source: .mockRandom(among: .alphanumerics)) + } +} + +extension URLRequestBuilder.HTTPHeader: RandomMockable, AnyMockable { + public static func mockRandom() -> Self { + let all: [URLRequestBuilder.HTTPHeader] = [ + .contentTypeHeader(contentType: Bool.random() ? .applicationJSON : .textPlainUTF8), + .userAgentHeader(appName: .mockRandom(among: .alphanumerics), appVersion: .mockRandom(among: .alphanumerics), device: .mockAny()), + .ddAPIKeyHeader(clientToken: .mockRandom(among: .alphanumerics)), + .ddEVPOriginHeader(source: .mockRandom(among: .alphanumerics)), + .ddEVPOriginVersionHeader(sdkVersion: .mockRandom(among: .alphanumerics)), + .ddRequestIDHeader() + ] + return all.randomElement()! + } + + public static func mockAny() -> Self { + return .ddEVPOriginVersionHeader(sdkVersion: "1.2.3") + } +} + +extension URLRequestBuilder: AnyMockable { + public static func mockAny() -> Self { + return mockWith() + } + + public static func mockWith( + url: URL = .mockAny(), + queryItems: [QueryItem] = [], + headers: [HTTPHeader] = [] + ) -> Self { + return URLRequestBuilder( + url: url, + queryItems: queryItems, + headers: headers + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift b/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift new file mode 100644 index 0000000000..e03165cf2c --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/DirectoriesMock.swift @@ -0,0 +1,101 @@ +/* + * 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 DatadogCore + +/// `CoreDirectory` pointing to subfolders in `/var/folders/`. +/// This location does not exist by default and should be created and deleted by calling `.create()` and `.delete()` in each test, +/// which guarantees clear state before and after test. +let temporaryCoreDirectory = temporaryUniqueCoreDirectory() + +/// `CoreDirectory` pointing to subfolders in `/var/folders/`. +/// This location does not exist by default and should be created and deleted by calling `.create()` and `.delete()` in each test, +func temporaryUniqueCoreDirectory(uuid: UUID = UUID()) -> CoreDirectory { + return CoreDirectory( + osDirectory: .init(url: obtainUniqueTemporaryDirectory()), + coreDirectory: .init(url: obtainUniqueTemporaryDirectory()) + ) +} + +extension CoreDirectory { + /// Creates temporary core directory. + @discardableResult + func create() -> Self { + osDirectory.create() + coreDirectory.create() + return self + } + + /// Deletes temporary core directory. + func delete() { + osDirectory.delete() + coreDirectory.delete() + } +} + +/// `FeatureDirectories` pointing to subfolders in `/var/folders/`. +/// Those subfolders do not exist by default and should be created and deleted by calling `.create()` and `.delete()` in each test, +/// which guarantees clear state before and after test. +let temporaryFeatureDirectories = FeatureDirectories( + unauthorized: .init(url: obtainUniqueTemporaryDirectory()), + authorized: .init(url: obtainUniqueTemporaryDirectory()) +) + +extension FeatureDirectories { + /// Creates temporary folder for each directory. + func create() { + authorized.create() + unauthorized.create() + } + + /// Deletes each temporary folder. + func delete() { + authorized.delete() + unauthorized.delete() + } +} + +/// Extends `Directory` with set of utilities for convenient work with files in tests. +/// Provides handy methods to create / delete files and directories. +extension Directory { + /// Creates empty directory with given attributes . + @discardableResult + func create(attributes: [FileAttributeKey: Any]? = nil, file: StaticString = #file, line: UInt = #line) -> Self { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes) + let initialFilesCount = try files().count + XCTAssert(initialFilesCount == 0, "🔥 `Directory` is not empty: \(url)", file: file, line: line) + } catch { + XCTFail("🔥 `Directory.create()` failed: \(error)", file: file, line: line) + } + return self + } + + /// Deletes entire directory with its content. + func delete(file: StaticString = #file, line: UInt = #line) { + if FileManager.default.fileExists(atPath: url.path) { + do { + try FileManager.default.removeItem(at: url) + } catch { + XCTFail("🔥 `Directory.delete()` failed: \(error)", file: file, line: line) + } + } + } + + /// Checks if directory exists. + func exists() -> Bool { + return FileManager.default.fileExists(atPath: url.path) + } + + func createMockFiles(count: Int, prefix: String = "file") { + (0.. Result + + /// Initializes the mock client with a result closure. + /// - Parameter result: Closure providing the completion result for each incoming request (default is a successful HTTP response with `202` code). + init(result: @escaping ((URLRequest) -> Result) = { _ in .success(.mockResponseWith(statusCode: 202)) }) { + self.result = result + } + + /// Convenience initializer for creating a mock client with a predefined response. + /// - Parameter response: `HTTPURLResponse` to be used as completion for all incoming requests. + convenience init(response: HTTPURLResponse) { + self.init(result: { _ in .success(response) }) + } + + /// Convenience initializer for creating a mock client with a predefined response code. + /// - Parameter responseCode: HTTP status code to be used as completion for all incoming requests. + convenience init(responseCode: Int) { + self.init(response: .mockResponseWith(statusCode: responseCode)) + } + + /// Convenience initializer for creating a mock client with a predefined error. + /// - Parameter error: Error to be used as completion for all incoming requests. + convenience init(error: Error) { + self.init(result: { _ in .failure(error) }) + } + + // MARK: - HTTPClient conformance + + func send(request: URLRequest, completion: @escaping (Result) -> Void) { + queue.async { + completion(self.result(request)) + self.requests.append(request) + } + } + + // MARK: - Tracked requests retrieval + + /// Retrieves the tracked requests. + /// - Returns: An array of tracked URLRequest instances. + /// - Throws: An error if decompression fails. + func requestsSent() -> [URLRequest] { + queue.sync { + self.requests + } + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/KronosClockMock.swift b/DatadogCore/Tests/Datadog/Mocks/KronosClockMock.swift new file mode 100644 index 0000000000..e47c0765c0 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/KronosClockMock.swift @@ -0,0 +1,46 @@ +/* + * 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 KronosClockMock: KronosClockProtocol { + typealias FirstCompletion = (Date, TimeInterval) -> Void + typealias EndCompletion = (Date?, TimeInterval?) -> Void + + var now: Date? { + offset.map { .init(timeIntervalSinceNow: $0) } + } + + private(set) var currentPool: String? = nil + private(set) var first: FirstCompletion? = nil + private(set) var completion: EndCompletion? = nil + private var offset: TimeInterval? = nil + + func update(offset: TimeInterval) { + self.offset = offset + + if let first = first { + first(.init(timeIntervalSinceNow: offset), offset) + self.first = nil + } + } + + func complete() { + completion?(now, offset) + } + + func sync( + from pool: String, + samples: Int, + first: FirstCompletion?, + completion: EndCompletion? + ) { + self.currentPool = pool + self.first = first + self.completion = completion + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift new file mode 100644 index 0000000000..0ef6a290df --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/LogsMocks.swift @@ -0,0 +1,318 @@ +/* + * 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 TestUtilities +import DatadogInternal + +@testable import DatadogLogs +@testable import DatadogCore + +extension DatadogCoreProxy { + func waitAndReturnLogMatchers(file: StaticString = #file, line: UInt = #line) throws -> [LogMatcher] { + return try waitAndReturnEventsData(ofFeature: LogsFeature.name) + .map { data in try LogMatcher.fromJSONObjectData(data) } + } +} + +extension LogsFeature { + /// Mocks an instance of the feature that performs no writes to file system and does no uploads. + static func mockAny() -> Self { .mockWith() } + + /// Mocks an instance of the feature that performs no writes to file system and does no uploads. + static func mockWith( + logEventMapper: LogEventMapper? = nil, + requestBuilder: FeatureRequestBuilder = RequestBuilder(), + messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), + dateProvider: DateProvider = SystemDateProvider(), + backtraceReporter: BacktraceReporting? = nil + ) -> Self { + return .init( + logEventMapper: logEventMapper, + requestBuilder: requestBuilder, + messageReceiver: messageReceiver, + dateProvider: dateProvider, + backtraceReporter: backtraceReporter + ) + } +} + +extension LogMessageReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + public static func mockWith( + logEventMapper: LogEventMapper? = nil + ) -> Self { + .init( + logEventMapper: logEventMapper + ) + } +} + +extension CrashLogReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + public static func mockWith( + dateProvider: DateProvider = SystemDateProvider() + ) -> Self { + .init( + dateProvider: dateProvider, + logEventMapper: nil + ) + } +} + +// MARK: - Log Mocks + +extension LogLevel: AnyMockable, RandomMockable { + public static func mockAny() -> LogLevel { + return .debug + } + + public static func mockRandom() -> LogLevel { + return [ + LogLevel.debug, + LogLevel.info, + LogLevel.notice, + LogLevel.warn, + LogLevel.error, + LogLevel.critical, + ].randomElement()! + } +} + +extension LogEvent: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent { + return mockWith() + } + + public static func mockWith( + date: Date = .mockAny(), + status: LogEvent.Status = .mockAny(), + message: String = .mockAny(), + error: LogEvent.Error? = nil, + serviceName: String = .mockAny(), + environment: String = .mockAny(), + loggerName: String = .mockAny(), + loggerVersion: String = .mockAny(), + threadName: String = .mockAny(), + applicationVersion: String = .mockAny(), + applicationBuildNumber: String = .mockAny(), + buildId: String? = .mockAny(), + variant: String? = .mockAny(), + dd: LogEvent.Dd = .mockAny(), + os: LogEvent.OperatingSystem = .mockAny(), + userInfo: UserInfo = .mockAny(), + networkConnectionInfo: NetworkConnectionInfo = .mockAny(), + mobileCarrierInfo: CarrierInfo? = .mockAny(), + attributes: LogEvent.Attributes = .mockAny(), + tags: [String]? = nil + ) -> LogEvent { + return LogEvent( + date: date, + status: status, + message: message, + error: error, + serviceName: serviceName, + environment: environment, + loggerName: loggerName, + loggerVersion: loggerVersion, + threadName: threadName, + applicationVersion: applicationVersion, + applicationBuildNumber: applicationBuildNumber, + buildId: buildId, + variant: variant, + dd: dd, + os: os, + userInfo: userInfo, + networkConnectionInfo: networkConnectionInfo, + mobileCarrierInfo: mobileCarrierInfo, + attributes: attributes, + tags: tags + ) + } + + public static func mockRandom() -> LogEvent { + return LogEvent( + date: .mockRandomInThePast(), + status: .mockRandom(), + message: .mockRandom(), + error: .mockRandom(), + serviceName: .mockRandom(), + environment: .mockRandom(), + loggerName: .mockRandom(), + loggerVersion: .mockRandom(), + threadName: .mockRandom(), + applicationVersion: .mockRandom(), + applicationBuildNumber: .mockRandom(), + buildId: .mockRandom(), + variant: .mockRandom(), + dd: .mockRandom(), + os: .mockRandom(), + userInfo: .mockRandom(), + networkConnectionInfo: .mockRandom(), + mobileCarrierInfo: .mockRandom(), + attributes: .mockRandom(), + tags: .mockRandom() + ) + } +} + +extension LogEvent.Status: RandomMockable { + public static func mockAny() -> LogEvent.Status { + return .info + } + + public static func mockRandom() -> LogEvent.Status { + return allCases.randomElement()! + } +} + +extension LogEvent.UserInfo: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.UserInfo { + return mockEmpty() + } + + public static func mockEmpty() -> LogEvent.UserInfo { + return LogEvent.UserInfo( + id: nil, + name: nil, + email: nil, + extraInfo: [:] + ) + } + + public static func mockRandom() -> LogEvent.UserInfo { + return .init( + id: .mockRandom(), + name: .mockRandom(), + email: .mockRandom(), + extraInfo: mockRandomAttributes() + ) + } +} + +extension LogEvent.Dd: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.Dd { + return LogEvent.Dd( + device: .mockAny() + ) + } + + public static func mockRandom() -> LogEvent.Dd { + return LogEvent.Dd( + device: .mockRandom() + ) + } +} + +extension LogEvent.OperatingSystem: AnyMockable, RandomMockable { + public static func mockAny() -> Self { + .init( + name: .mockAny(), + version: .mockAny(), + build: .mockAny() + ) + } + + public static func mockRandom() -> Self { + .init( + name: .mockRandom(), + version: .mockRandom(), + build: .mockRandom() + ) + } +} + +extension LogEvent.DeviceInfo: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.DeviceInfo { + return LogEvent.DeviceInfo( + brand: .mockAny(), + name: .mockAny(), + model: .mockAny(), + architecture: .mockAny() + ) + } + + public static func mockRandom() -> LogEvent.DeviceInfo { + return LogEvent.DeviceInfo( + brand: .mockRandom(), + name: .mockRandom(), + model: .mockRandom(), + architecture: .mockRandom() + ) + } +} + +extension LogEvent.Error: RandomMockable { + public static func mockRandom() -> Self { + return .init( + kind: .mockRandom(), + message: .mockRandom(), + stack: .mockRandom() + ) + } +} + +// MARK: - Component Mocks + +extension LogEventBuilder: AnyMockable { + public static func mockAny() -> LogEventBuilder { + return mockWith() + } + + public static func mockWith( + service: String = .mockAny(), + loggerName: String = .mockAny(), + networkInfoEnabled: Bool = .mockAny(), + eventMapper: LogEventMapper? = nil, + deviceInfo: DeviceInfo = .mockAny() + ) -> LogEventBuilder { + return LogEventBuilder( + service: service, + loggerName: loggerName, + networkInfoEnabled: networkInfoEnabled, + eventMapper: eventMapper + ) + } +} + +extension LogEvent.Attributes: Equatable { + public static func mockAny() -> LogEvent.Attributes { + return mockWith() + } + + public static func mockWith( + userAttributes: [String: Encodable] = [:], + internalAttributes: [String: Encodable]? = [:] + ) -> LogEvent.Attributes { + return LogEvent.Attributes( + userAttributes: userAttributes, + internalAttributes: internalAttributes + ) + } + + public static func mockRandom() -> LogEvent.Attributes { + return .init( + userAttributes: mockRandomAttributes(), + internalAttributes: mockRandomAttributes() + ) + } + + public static func == (lhs: LogEvent.Attributes, rhs: LogEvent.Attributes) -> Bool { + let lhsUserAttributesSorted = lhs.userAttributes.sorted { $0.key < $1.key } + let rhsUserAttributesSorted = rhs.userAttributes.sorted { $0.key < $1.key } + + let lhsInternalAttributesSorted = lhs.internalAttributes?.sorted { $0.key < $1.key } + let rhsInternalAttributesSorted = rhs.internalAttributes?.sorted { $0.key < $1.key } + + return String(describing: lhsUserAttributesSorted) == String(describing: rhsUserAttributesSorted) + && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift new file mode 100644 index 0000000000..2936adc277 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -0,0 +1,607 @@ +/* + * 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 TestUtilities + +@testable import DatadogRUM + +extension RUMUser { + static func mockRandom() -> RUMUser { + return RUMUser( + anonymousId: .mockRandom(), + email: .mockRandom(), + id: .mockRandom(), + name: .mockRandom(), + usrInfo: mockRandomAttributes() + ) + } +} + +extension RUMConnectivity { + static func mockRandom() -> RUMConnectivity { + return RUMConnectivity( + cellular: .init( + carrierName: .mockRandom(), + technology: .mockRandom() + ), + effectiveType: nil, + interfaces: [.bluetooth, .cellular].randomElements(), + status: [.connected, .maybe, .notConnected].randomElement()! + ) + } +} + +extension RUMMethod: RandomMockable { + public static func mockRandom() -> RUMMethod { + return [.post, .get, .head, .put, .delete, .patch].randomElement()! + } +} + +extension RUMSessionPrecondition: RandomMockable { + public static func mockRandom() -> RUMSessionPrecondition { + return [.userAppLaunch, .inactivityTimeout, .maxDuration, .backgroundLaunch, .prewarm, .fromNonInteractiveSession, .explicitStop].randomElement()! + } +} + +extension RUMEventAttributes: RandomMockable { + public static func mockRandom() -> RUMEventAttributes { + return .init(contextInfo: mockRandomAttributes()) + } +} + +extension RUMDevice: RandomMockable { + public static func mockRandom() -> RUMDevice { + return .init( + architecture: .mockRandom(), + brand: .mockRandom(), + model: .mockRandom(), + name: .mockRandom(), + type: .mockRandom() + ) + } +} + +extension RUMActionID: RandomMockable { + public static func mockRandom() -> RUMActionID { + if Bool.random() { + return .string(value: .mockRandom()) + } else { + return .stringsArray(value: .mockRandom()) + } + } +} + +extension RUMActionID { + var stringValue: String? { + switch self { + case .string(let value): + return value + default: + return nil + } + } +} + +extension RUMDevice.RUMDeviceType: RandomMockable { + public static func mockRandom() -> RUMDevice.RUMDeviceType { + return [.mobile, .desktop, .tablet, .tv, .gamingConsole, .bot, .other].randomElement()! + } +} + +extension RUMOperatingSystem: RandomMockable { + public static func mockRandom() -> RUMOperatingSystem { + return .init( + build: nil, + name: .mockRandom(length: 5), + version: .mockRandom(among: .decimalDigits, length: 2), + versionMajor: .mockRandom(among: .decimalDigits, length: 1) + ) + } +} + +extension RUMViewEvent.DD.Configuration: RandomMockable { + public static func mockRandom() -> RUMViewEvent.DD.Configuration { + return .init( + sessionReplaySampleRate: .mockRandom(min: 0, max: 100), + sessionSampleRate: .mockRandom(min: 0, max: 100), + startSessionReplayRecordingManually: nil + ) + } +} + +extension RUMViewEvent: RandomMockable { + public static func mockRandom() -> RUMViewEvent { + return mockRandomWith() + } + + /// Produces random `RUMViewEvent` with setting given fields to certain values. + static func mockRandomWith( + viewIsActive: Bool? = .random(), + viewTimeSpent: Int64 = .mockRandom(), + crashCount: Int64? = nil + ) -> RUMViewEvent { + return RUMViewEvent( + dd: .init( + browserSdkVersion: nil, + configuration: .mockRandom(), + documentVersion: .mockRandom(), + pageStates: nil, + replayStats: nil, + session: .init( + plan: .plan1, + sessionPrecondition: .mockRandom() + ) + ), + application: .init(id: .mockRandom()), + buildId: nil, + buildVersion: .mockRandom(), + ciTest: nil, + connectivity: .mockRandom(), + container: nil, + context: .mockRandom(), + date: .mockRandom(), + device: .mockRandom(), + display: nil, + os: .mockRandom(), + privacy: nil, + service: .mockRandom(), + session: .init( + hasReplay: nil, + id: .mockRandom(), + isActive: true, + sampledForReplay: nil, + type: .user + ), + source: .ios, + synthetics: nil, + usr: .mockRandom(), + version: .mockAny(), + view: .init( + action: .init(count: .mockRandom()), + cpuTicksCount: .mockRandom(), + cpuTicksPerSecond: .mockRandom(), + crash: crashCount.map { .init(count: $0) }, + cumulativeLayoutShift: .mockRandom(), + cumulativeLayoutShiftTargetSelector: nil, + cumulativeLayoutShiftTime: .mockRandom(), + customTimings: .mockAny(), + domComplete: .mockRandom(), + domContentLoaded: .mockRandom(), + domInteractive: .mockRandom(), + error: .init(count: .mockRandom()), + firstByte: .mockRandom(), + firstContentfulPaint: .mockRandom(), + firstInputDelay: .mockRandom(), + firstInputTargetSelector: nil, + firstInputTime: .mockRandom(), + flutterBuildTime: nil, + flutterRasterTime: nil, + frozenFrame: .init(count: .mockRandom()), + frustration: nil, + id: .mockRandom(), + inForegroundPeriods: [ + .init( + duration: .mockRandom(), + start: .mockRandom() + ) + ], + interactionToNextPaint: nil, + interactionToNextPaintTargetSelector: nil, + interactionToNextPaintTime: .mockRandom(), + interactionToNextViewTime: nil, + isActive: viewIsActive, + isSlowRendered: .mockRandom(), + jsRefreshRate: nil, + largestContentfulPaint: .mockRandom(), + largestContentfulPaintTargetSelector: nil, + loadEvent: .mockRandom(), + loadingTime: viewTimeSpent, + loadingType: nil, + longTask: .init(count: .mockRandom()), + memoryAverage: .mockRandom(), + memoryMax: .mockRandom(), + name: .mockRandom(), + networkSettledTime: nil, + referrer: .mockRandom(), + refreshRateAverage: .mockRandom(), + refreshRateMin: .mockRandom(), + resource: .init(count: .mockRandom()), + timeSpent: viewTimeSpent, + url: .mockRandom() + ) + ) + } +} + +extension RUMResourceEvent.DD.Configuration: RandomMockable { + public static func mockRandom() -> RUMResourceEvent.DD.Configuration { + .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) + } +} + +extension RUMResourceEvent: RandomMockable { + public static func mockRandom() -> RUMResourceEvent { + return RUMResourceEvent( + dd: .init( + browserSdkVersion: nil, + configuration: .mockRandom(), + discarded: nil, + rulePsr: nil, + session: .init( + plan: [.plan1, .plan2].randomElement()!, + sessionPrecondition: .mockRandom() + ), + spanId: .mockRandom(), + traceId: .mockRandom() + ), + action: .init(id: .mockRandom()), + application: .init(id: .mockRandom()), + buildId: nil, + buildVersion: .mockRandom(), + ciTest: nil, + connectivity: .mockRandom(), + container: nil, + context: .mockRandom(), + date: .mockRandom(), + device: .mockRandom(), + display: nil, + os: .mockRandom(), + 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(), + encodedBodySize: nil, + firstByte: .init(duration: .mockRandom(), start: .mockRandom()), + id: .mockRandom(), + method: .mockRandom(), + protocol: nil, + provider: .init( + domain: .mockRandom(), + name: .mockRandom(), + type: Bool.random() ? .firstParty : nil + ), + redirect: .init(duration: .mockRandom(), start: .mockRandom()), + renderBlockingStatus: nil, + size: .mockRandom(), + ssl: .init(duration: .mockRandom(), start: .mockRandom()), + statusCode: .mockRandom(), + transferSize: nil, + type: [.native, .image].randomElement()!, + url: .mockRandom(), + worker: nil + ), + service: .mockRandom(), + session: .init( + hasReplay: nil, + id: .mockRandom(), + type: .user + ), + source: .ios, + synthetics: nil, + usr: .mockRandom(), + version: .mockAny(), + view: .init( + id: .mockRandom(), + referrer: .mockRandom(), + url: .mockRandom() + ) + ) + } +} + +extension RUMActionEvent.DD.Configuration: RandomMockable { + public static func mockRandom() -> RUMActionEvent.DD.Configuration { + .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) + } +} + +extension RUMActionEvent: RandomMockable { + public static func mockRandom() -> RUMActionEvent { + return RUMActionEvent( + dd: .init( + action: .init( + position: nil, + target: .init( + height: nil, + selector: nil, + width: .mockRandom() + ) + ), + browserSdkVersion: nil, + configuration: .mockRandom(), + session: .init( + plan: [.plan1, .plan2].randomElement()!, + sessionPrecondition: .mockRandom() + ) + ), + action: .init( + crash: .init(count: .mockRandom()), + error: .init(count: .mockRandom()), + frustration: nil, + id: .mockRandom(), + loadingTime: .mockRandom(), + longTask: .init(count: .mockRandom()), + resource: .init(count: .mockRandom()), + target: .init(name: .mockRandom()), + type: [.tap, .swipe, .scroll].randomElement()! + ), + application: .init(id: .mockRandom()), + buildId: nil, + buildVersion: .mockRandom(), + ciTest: nil, + connectivity: .mockRandom(), + container: nil, + context: .mockRandom(), + date: .mockRandom(), + device: .mockRandom(), + display: nil, + os: .mockRandom(), + service: .mockRandom(), + session: .init( + hasReplay: nil, + id: .mockRandom(), + type: .user + ), + source: .ios, + synthetics: nil, + usr: .mockRandom(), + version: .mockAny(), + view: .init( + id: .mockRandom(), + inForeground: .random(), + referrer: .mockRandom(), + url: .mockRandom() + ) + ) + } +} + +extension RUMErrorEvent.Error.SourceType: RandomMockable { + public static func mockRandom() -> RUMErrorEvent.Error.SourceType { + return [.android, .browser, .ios, .reactNative].randomElement()! + } +} + +extension RUMErrorEvent.DD.Configuration: RandomMockable { + public static func mockRandom() -> RUMErrorEvent.DD.Configuration { + .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) + } +} + +extension RUMErrorEvent: RandomMockable { + public static func mockRandom() -> RUMErrorEvent { + return RUMErrorEvent( + dd: .init( + browserSdkVersion: nil, + configuration: .mockRandom(), + session: .init( + plan: [.plan1, .plan2].randomElement()!, + sessionPrecondition: .mockRandom() + ) + ), + action: .init(id: .mockRandom()), + application: .init(id: .mockRandom()), + buildId: nil, + buildVersion: .mockRandom(), + ciTest: nil, + connectivity: .mockRandom(), + container: nil, + context: .mockRandom(), + date: .mockRandom(), + device: .mockRandom(), + display: nil, + error: .init( + binaryImages: nil, + category: nil, + csp: nil, + handling: nil, + handlingStack: nil, + id: .mockRandom(), + isCrash: .random(), + message: .mockRandom(), + meta: nil, + resource: .init( + method: .mockRandom(), + provider: .init( + domain: .mockRandom(), + name: .mockRandom(), + type: Bool.random() ? .firstParty : nil + ), + statusCode: .mockRandom(), + url: .mockRandom() + ), + source: [.source, .network, .custom].randomElement()!, + sourceType: .mockRandom(), + stack: .mockRandom(), + threads: nil, + timeSinceAppStart: nil, + type: .mockRandom(), + wasTruncated: .mockRandom() + ), + freeze: nil, + os: .mockRandom(), + service: .mockRandom(), + session: .init( + hasReplay: nil, + id: .mockRandom(), + type: .user + ), + source: .ios, + synthetics: nil, + usr: .mockRandom(), + version: .mockAny(), + view: .init( + id: .mockRandom(), + inForeground: .random(), + referrer: .mockRandom(), + url: .mockRandom() + ) + ) + } +} + +extension RUMLongTaskEvent.DD.Configuration: RandomMockable { + public static func mockRandom() -> RUMLongTaskEvent.DD.Configuration { + return .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) + } +} + +extension RUMLongTaskEvent: RandomMockable { + public static func mockRandom() -> RUMLongTaskEvent { + return RUMLongTaskEvent( + dd: .init( + browserSdkVersion: nil, + configuration: .mockRandom(), + discarded: nil, + session: .init( + plan: [.plan1, .plan2].randomElement()!, + sessionPrecondition: .mockRandom() + ) + ), + action: .init(id: .mockRandom()), + application: .init(id: .mockRandom()), + buildId: nil, + buildVersion: .mockRandom(), + ciTest: nil, + connectivity: .mockRandom(), + container: nil, + context: .mockRandom(), + date: .mockRandom(), + device: .mockRandom(), + display: nil, + 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( + hasReplay: false, + id: .mockRandom(), + type: .user + ), + source: .ios, + synthetics: nil, + usr: .mockRandom(), + version: .mockAny(), + view: .init(id: .mockRandom(), name: .mockRandom(), referrer: .mockRandom(), url: .mockRandom()) + ) + } +} + +extension TelemetryConfigurationEvent: RandomMockable { + public static func mockRandom() -> TelemetryConfigurationEvent { + return TelemetryConfigurationEvent( + dd: .init(), + action: .init(id: .mockRandom()), + application: .init(id: .mockRandom()), + date: .mockRandom(), + effectiveSampleRate: .mockRandom(), + experimentalFeatures: nil, + service: .mockRandom(), + session: .init(id: .mockRandom()), + source: .ios, + telemetry: .init( + configuration: .init( + actionNameAttribute: nil, + allowFallbackToLocalStorage: nil, + allowUntrustedEvents: nil, + appHangThreshold: .mockRandom(), + backgroundTasksEnabled: .mockRandom(), + 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, + reactVersion: nil, + replaySampleRate: nil, + selectedTracingPropagators: nil, + sessionReplaySampleRate: nil, + sessionSampleRate: .mockRandom(), + silentMultipleInit: nil, + storeContextsAcrossPages: nil, + telemetryConfigurationSampleRate: .mockRandom(), + telemetrySampleRate: .mockRandom(), + telemetryUsageSampleRate: nil, + traceSampleRate: .mockRandom(), + trackBackgroundEvents: .mockRandom(), + trackCrossPlatformLongTasks: .mockRandom(), + trackErrors: .mockRandom(), + trackFlutterPerformance: .mockRandom(), + trackFrustrations: .mockRandom(), + trackInteractions: .mockRandom(), + trackLongTask: .mockRandom(), + trackNativeErrors: .mockRandom(), + trackNativeLongTasks: .mockRandom(), + trackNativeViews: .mockRandom(), + trackNetworkRequests: .mockRandom(), + trackResources: .mockRandom(), + trackSessionAcrossSubdomains: nil, + trackViewsManually: nil, + trackingConsent: nil, + useAllowedTracingOrigins: .mockRandom(), + useAllowedTracingUrls: nil, + useBeforeSend: nil, + useCrossSiteSessionCookie: nil, + useExcludedActivityUrls: nil, + useFirstPartyHosts: .mockRandom(), + useLocalEncryption: .mockRandom(), + usePartitionedCrossSiteSessionCookie: .mockRandom(), + useProxy: .mockRandom(), + useSecureSessionCookie: nil, + useTracing: .mockRandom(), + useWorkerUrl: nil, + viewTrackingStrategy: nil + ), + device: .mockRandom(), + os: .mockRandom(), + telemetryInfo: [:] + ), + version: .mockAny(), + view: .init(id: .mockRandom()) + ) + } +} + +extension RUMTelemetryDevice: RandomMockable { + public static func mockRandom() -> RUMTelemetryDevice { + return RUMTelemetryDevice( + architecture: .mockRandom(), + brand: .mockRandom(), + model: .mockRandom() + ) + } +} + +extension RUMTelemetryOperatingSystem: RandomMockable { + public static func mockRandom() -> RUMTelemetryOperatingSystem { + return RUMTelemetryOperatingSystem( + build: .mockRandom(), + name: .mockRandom(), + version: .mockRandom() + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift new file mode 100644 index 0000000000..7ef7b71127 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -0,0 +1,1090 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogRUM + +extension DatadogCoreProxy { + func waitAndReturnRUMEventMatchers(file: StaticString = #file, line: UInt = #line) throws -> [RUMEventMatcher] { + return try waitAndReturnEventsData(ofFeature: RUMFeature.name) + .map { data in try RUMEventMatcher.fromJSONObjectData(data) } + } +} + +extension RUM.Configuration { + static func mockAny() -> RUM.Configuration { + return mockWith { _ in } + } + + static func mockWith( + applicationID: String = .mockAny(), + mutation: (inout RUM.Configuration) -> Void + ) -> RUM.Configuration { + var config = RUM.Configuration(applicationID: applicationID) + mutation(&config) + return config + } +} + +extension WebViewEventReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + static func mockWith( + featureScope: FeatureScope = NOPFeatureScope(), + dateProvider: DateProvider = SystemDateProvider(), + commandSubscriber: RUMCommandSubscriber = RUMCommandSubscriberMock(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()) + ) -> Self { + .init( + featureScope: featureScope, + dateProvider: dateProvider, + commandSubscriber: commandSubscriber, + viewCache: viewCache + ) + } +} + +extension CrashReportReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + static func mockWith( + featureScope: FeatureScope = NOPFeatureScope(), + applicationID: String = .mockAny(), + dateProvider: DateProvider = SystemDateProvider(), + sessionSampler: Sampler = .mockKeepAll(), + trackBackgroundEvents: Bool = true, + uuidGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), + ciTest: RUMCITest? = nil, + syntheticsTest: RUMSyntheticsTest? = nil, + eventsMapper: RUMEventsMapper = .mockNoOp() + ) -> Self { + .init( + featureScope: featureScope, + applicationID: applicationID, + dateProvider: dateProvider, + sessionSampler: sessionSampler, + trackBackgroundEvents: trackBackgroundEvents, + uuidGenerator: uuidGenerator, + ciTest: ciTest, + syntheticsTest: syntheticsTest, + eventsMapper: eventsMapper + ) + } +} + +// MARK: - Public API Mocks + +extension RUMMethod { + static func mockAny() -> RUMMethod { .get } +} + +extension RUMResourceType { + static func mockAny() -> RUMResourceType { .image } +} + +// MARK: - RUMDataModel Mocks + +struct RUMDataModelMock: RUMDataModel, RUMSanitizableEvent { + let attribute: String + var usr: RUMUser? + var context: RUMEventAttributes? +} + +// MARK: - Component Mocks + +extension RUMEventBuilder { + static func mockAny() -> RUMEventBuilder { + return RUMEventBuilder(eventsMapper: .mockNoOp()) + } +} + +extension RUMEventsMapper { + static func mockNoOp() -> RUMEventsMapper { + return mockWith() + } + + static func mockWith( + viewEventMapper: RUM.ViewEventMapper? = nil, + errorEventMapper: RUM.ErrorEventMapper? = nil, + resourceEventMapper: RUM.ResourceEventMapper? = nil, + actionEventMapper: RUM.ActionEventMapper? = nil, + longTaskEventMapper: RUM.LongTaskEventMapper? = nil, + telemetry: Telemetry = NOPTelemetry() + ) -> RUMEventsMapper { + return RUMEventsMapper( + viewEventMapper: viewEventMapper, + errorEventMapper: errorEventMapper, + resourceEventMapper: resourceEventMapper, + actionEventMapper: actionEventMapper, + longTaskEventMapper: longTaskEventMapper, + telemetry: telemetry + ) + } +} + +// MARK: - RUMCommand Mocks + +/// Holds the `mockView` object so it can be weakly referenced by `RUMViewScope` mocks. +let mockView: UIViewController = createMockViewInWindow() + +extension ViewIdentifier { + static func mockViewIdentifier() -> ViewIdentifier { + ViewIdentifier(mockView) + } + + static func mockRandomString() -> ViewIdentifier { + ViewIdentifier(String.mockRandom()) + } +} + +struct RUMCommandMock: RUMCommand { + var time = Date() + var attributes: [AttributeKey: AttributeValue] = [:] + var canStartBackgroundView = false + var isUserInteraction = false + var missedEventType: SessionEndedMetric.MissedEventType? = nil +} + +/// Creates random `RUMCommand` from available ones. +func mockRandomRUMCommand(where predicate: (RUMCommand) -> Bool = { _ in true }) -> RUMCommand { + let allCommands: [RUMCommand] = [ + RUMStartViewCommand.mockRandom(), + RUMStopViewCommand.mockRandom(), + RUMAddCurrentViewErrorCommand.mockRandom(), + RUMAddViewTimingCommand.mockRandom(), + RUMStartResourceCommand.mockRandom(), + RUMAddResourceMetricsCommand.mockRandom(), + RUMStopResourceCommand.mockRandom(), + RUMStopResourceWithErrorCommand.mockRandom(), + RUMStartUserActionCommand.mockRandom(), + RUMStopUserActionCommand.mockRandom(), + RUMAddUserActionCommand.mockRandom(), + RUMAddLongTaskCommand.mockRandom(), + ] + return allCommands.filter(predicate).randomElement()! +} + +extension RUMCommand { + func replacing(time: Date? = nil, attributes: [AttributeKey: AttributeValue]? = nil) -> RUMCommand { + var command = self + command.time = time ?? command.time + command.attributes = attributes ?? command.attributes + return command + } +} + +extension RUMStartViewCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStartViewCommand { mockWith() } + + public static func mockRandom() -> RUMStartViewCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + identity: .mockRandomString(), + name: .mockRandom(), + path: .mockRandom() + ) + } + + static func mockWith( + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + identity: ViewIdentifier = .mockViewIdentifier(), + name: String = .mockAny(), + path: String = .mockAny(), + instrumentationType: SessionEndedMetric.ViewInstrumentationType = .manual + ) -> RUMStartViewCommand { + return RUMStartViewCommand( + time: time, + identity: identity, + name: name, + path: path, + attributes: attributes, + instrumentationType: instrumentationType + ) + } +} + +extension RUMStopViewCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStopViewCommand { mockWith() } + + public static func mockRandom() -> RUMStopViewCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + identity: .mockRandomString() + ) + } + + static func mockWith( + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + identity: ViewIdentifier = .mockViewIdentifier() + ) -> RUMStopViewCommand { + return RUMStopViewCommand( + time: time, attributes: attributes, identity: identity + ) + } +} + +extension RUMAddCurrentViewErrorCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddCurrentViewErrorCommand { .mockWithErrorObject() } + + public static func mockRandom() -> RUMAddCurrentViewErrorCommand { + if Bool.random() { + return .mockWithErrorObject( + time: .mockRandomInThePast(), + error: ErrorMock(.mockRandom()), + source: .mockRandom(), + attributes: mockRandomAttributes() + ) + } else { + return .mockWithErrorMessage( + time: .mockRandomInThePast(), + message: .mockRandom(), + type: .mockRandom(), + source: .mockRandom(), + stack: .mockRandom(), + attributes: mockRandomAttributes() + ) + } + } + + static func mockWithErrorObject( + time: Date = Date(), + error: Error = ErrorMock(), + source: RUMInternalErrorSource = .source, + attributes: [AttributeKey: AttributeValue] = [:] + ) -> RUMAddCurrentViewErrorCommand { + return RUMAddCurrentViewErrorCommand( + time: time, + error: error, + source: source, + attributes: attributes + ) + } + + static func mockWithErrorMessage( + time: Date = Date(), + message: String = .mockAny(), + type: String? = .mockAny(), + source: RUMInternalErrorSource = .source, + stack: String? = "Foo.swift:10", + attributes: [AttributeKey: AttributeValue] = [:] + ) -> RUMAddCurrentViewErrorCommand { + return RUMAddCurrentViewErrorCommand( + time: time, + message: message, + type: type, + stack: stack, + source: source, + attributes: attributes + ) + } +} + +extension RUMAddViewTimingCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddViewTimingCommand { .mockWith() } + + public static func mockRandom() -> RUMAddViewTimingCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + timingName: .mockRandom() + ) + } + + static func mockWith( + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + timingName: String = .mockAny() + ) -> RUMAddViewTimingCommand { + return RUMAddViewTimingCommand( + time: time, attributes: attributes, timingName: timingName + ) + } +} + +extension RUMSpanContext: AnyMockable, RandomMockable { + public static func mockAny() -> RUMSpanContext { + return .mockWith() + } + + public static func mockRandom() -> RUMSpanContext { + return RUMSpanContext( + traceID: .mock(.mockRandom(), .mockRandom()), + spanID: .mock(.mockRandom()), + samplingRate: .mockRandom() + ) + } + + static func mockWith( + traceID: TraceID = .mockAny(), + spanID: SpanID = .mockAny(), + samplingRate: Double = .mockAny() + ) -> RUMSpanContext { + return RUMSpanContext( + traceID: traceID, + spanID: spanID, + samplingRate: samplingRate + ) + } +} + +extension RUMStartResourceCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStartResourceCommand { mockWith() } + + public static func mockRandom() -> RUMStartResourceCommand { + return .mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + url: .mockRandom(), + httpMethod: .mockRandom(), + kind: .mockAny(), + isFirstPartyRequest: .mockRandom(), + spanContext: .init( + traceID: .mock(.mockRandom(), .mockRandom()), + spanID: .mock(.mockRandom()), + samplingRate: .mockAny() + ) + ) + } + + static func mockWith( + resourceKey: String = .mockAny(), + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + url: String = .mockAny(), + httpMethod: RUMMethod = .mockAny(), + kind: RUMResourceType = .mockAny(), + isFirstPartyRequest: Bool = .mockAny(), + spanContext: RUMSpanContext? = .mockAny() + ) -> RUMStartResourceCommand { + return RUMStartResourceCommand( + resourceKey: resourceKey, + time: time, + attributes: attributes, + url: url, + httpMethod: httpMethod, + kind: kind, + spanContext: spanContext + ) + } +} + +extension RUMAddResourceMetricsCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddResourceMetricsCommand { mockWith() } + + public static func mockRandom() -> RUMAddResourceMetricsCommand { + return mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + metrics: .mockAny() + ) + } + + static func mockWith( + resourceKey: String = .mockAny(), + time: Date = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + metrics: ResourceMetrics = .mockAny() + ) -> RUMAddResourceMetricsCommand { + return RUMAddResourceMetricsCommand( + resourceKey: resourceKey, + time: time, + attributes: attributes, + metrics: metrics + ) + } +} + +extension RUMStopResourceCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStopResourceCommand { mockWith() } + + public static func mockRandom() -> RUMStopResourceCommand { + return mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + kind: [.native, .image, .font, .other].randomElement()!, + httpStatusCode: .mockRandom(), + size: .mockRandom() + ) + } + + static func mockWith( + resourceKey: String = .mockAny(), + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + kind: RUMResourceType = .mockAny(), + httpStatusCode: Int? = .mockAny(), + size: Int64? = .mockAny() + ) -> RUMStopResourceCommand { + return RUMStopResourceCommand( + resourceKey: resourceKey, time: time, attributes: attributes, kind: kind, httpStatusCode: httpStatusCode, size: size + ) + } +} + +extension RUMStopResourceWithErrorCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStopResourceWithErrorCommand { mockWithErrorMessage() } + + public static func mockRandom() -> RUMStopResourceWithErrorCommand { + if Bool.random() { + return mockWithErrorObject( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + error: ErrorMock(.mockRandom()), + source: .mockRandom(), + httpStatusCode: .mockRandom(), + attributes: mockRandomAttributes() + ) + } else { + return mockWithErrorMessage( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + message: .mockRandom(), + type: .mockRandom(), + source: .mockRandom(), + httpStatusCode: .mockRandom(), + attributes: mockRandomAttributes() + ) + } + } + + static func mockWithErrorObject( + resourceKey: String = .mockAny(), + time: Date = Date(), + error: Error = ErrorMock(), + source: RUMInternalErrorSource = .source, + httpStatusCode: Int? = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:] + ) -> RUMStopResourceWithErrorCommand { + return RUMStopResourceWithErrorCommand( + resourceKey: resourceKey, time: time, error: error, source: source, httpStatusCode: httpStatusCode, attributes: attributes + ) + } + + static func mockWithErrorMessage( + resourceKey: String = .mockAny(), + time: Date = Date(), + message: String = .mockAny(), + type: String? = .mockAny(), + source: RUMInternalErrorSource = .source, + httpStatusCode: Int? = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:] + ) -> RUMStopResourceWithErrorCommand { + return RUMStopResourceWithErrorCommand( + resourceKey: resourceKey, time: time, message: message, type: type, source: source, httpStatusCode: httpStatusCode, attributes: attributes + ) + } +} + +extension RUMStartUserActionCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStartUserActionCommand { mockWith() } + + public static func mockRandom() -> RUMStartUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.swipe, .scroll, .custom].randomElement()!, + name: .mockRandom() + ) + } + + 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, instrumentation: instrumentation, actionType: actionType, name: name + ) + } +} + +extension RUMStopUserActionCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMStopUserActionCommand { mockWith() } + + public static func mockRandom() -> RUMStopUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.swipe, .scroll, .custom].randomElement()!, + name: .mockRandom() + ) + } + + static func mockWith( + time: Date = Date(), + attributes: [AttributeKey: AttributeValue] = [:], + actionType: RUMActionType = .swipe, + name: String? = nil + ) -> RUMStopUserActionCommand { + return RUMStopUserActionCommand( + time: time, attributes: attributes, actionType: actionType, name: name + ) + } +} + +extension RUMAddUserActionCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddUserActionCommand { mockWith() } + + public static func mockRandom() -> RUMAddUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.tap, .custom].randomElement()!, + name: .mockRandom() + ) + } + + 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, instrumentation: instrumentation, actionType: actionType, name: name + ) + } +} + +extension RUMAddLongTaskCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddLongTaskCommand { mockWith() } + + public static func mockRandom() -> RUMAddLongTaskCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + duration: .mockRandom(min: 0.01, max: 1) + ) + } + + static func mockWith( + time: Date = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + duration: TimeInterval = 0.01 + ) -> RUMAddLongTaskCommand { + return RUMAddLongTaskCommand( + time: time, + attributes: attributes, + duration: duration + ) + } +} + +extension RUMAddFeatureFlagEvaluationCommand: AnyMockable, RandomMockable { + public static func mockAny() -> RUMAddFeatureFlagEvaluationCommand { mockWith() } + + public static func mockRandom() -> RUMAddFeatureFlagEvaluationCommand { + return mockWith( + time: .mockRandomInThePast(), + name: .mockRandom(), + value: String.mockRandom() + ) + } + + static func mockWith( + time: Date = .mockAny(), + name: String = .mockAny(), + value: Encodable = String.mockAny() + ) -> RUMAddFeatureFlagEvaluationCommand { + return RUMAddFeatureFlagEvaluationCommand( + time: time, + name: name, + value: value + ) + } +} + +extension RUMStopSessionCommand: AnyMockable { + public static func mockAny() -> RUMStopSessionCommand { mockWith() } + + static func mockWith(time: Date = .mockAny()) -> RUMStopSessionCommand { + return RUMStopSessionCommand(time: time) + } +} + +// MARK: - RUMCommand Property Mocks + +extension RUMInternalErrorSource: RandomMockable { + public static func mockRandom() -> RUMInternalErrorSource { + return [.custom, .source, .network, .webview, .logger, .console].randomElement()! + } +} + +// MARK: - RUMContext Mocks + +extension RUMUUID { + public static func mockRandom() -> RUMUUID { + return RUMUUID(rawValue: UUID()) + } +} + +struct RUMUUIDGeneratorMock: RUMUUIDGenerator { + let uuid: UUID + func generateUnique() -> RUMUUID { RUMUUID(rawValue: uuid) } +} + +extension RUMContext { + public static func mockAny() -> RUMContext { + return mockWith() + } + + static func mockWith( + rumApplicationID: String = .mockAny(), + sessionID: RUMUUID = .mockRandom(), + isSessionActive: Bool = true, + activeViewID: RUMUUID? = nil, + activeViewPath: String? = nil, + activeViewName: String? = nil, + activeUserActionID: RUMUUID? = nil + ) -> RUMContext { + return RUMContext( + rumApplicationID: rumApplicationID, + sessionID: sessionID, + isSessionActive: true, + activeViewID: activeViewID, + activeViewPath: activeViewPath, + activeViewName: activeViewName, + activeUserActionID: activeUserActionID + ) + } +} + +extension RUMSessionState: AnyMockable, RandomMockable { + public static func mockAny() -> RUMSessionState { + return mockWith() + } + + public static func mockRandom() -> RUMSessionState { + return .init( + sessionUUID: .mockRandom(), + isInitialSession: .mockRandom(), + hasTrackedAnyView: .mockRandom(), + didStartWithReplay: .mockRandom() + ) + } + + static func mockWith( + sessionUUID: UUID = .mockAny(), + isInitialSession: Bool = .mockAny(), + hasTrackedAnyView: Bool = .mockAny(), + didStartWithReplay: Bool? = .mockAny() + ) -> RUMSessionState { + return RUMSessionState( + sessionUUID: sessionUUID, + isInitialSession: isInitialSession, + hasTrackedAnyView: hasTrackedAnyView, + didStartWithReplay: didStartWithReplay + ) + } +} + +// MARK: - RUMScope Mocks + +func mockNoOpSessionListener() -> RUM.SessionListener { + return { _, _ in } +} + +internal class FatalErrorContextNotifierMock: FatalErrorContextNotifying { + var sessionState: RUMSessionState? + var view: RUMViewEvent? + var globalAttributes: [String: Encodable] = [:] +} + +extension RUMScopeDependencies { + static func mockAny() -> RUMScopeDependencies { + return mockWith() + } + + static func mockWith( + featureScope: FeatureScope = NOPFeatureScope(), + rumApplicationID: String = .mockAny(), + sessionSampler: Sampler = .mockKeepAll(), + trackBackgroundEvents: Bool = .mockAny(), + trackFrustrations: Bool = true, + firstPartyHosts: FirstPartyHosts = .init([:]), + eventBuilder: RUMEventBuilder = RUMEventBuilder(eventsMapper: .mockNoOp()), + rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), + backtraceReporter: BacktraceReporting = BacktraceReporterMock(backtrace: nil), + ciTest: RUMCITest? = nil, + syntheticsTest: RUMSyntheticsTest? = nil, + vitalsReaders: VitalsReaders? = nil, + onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), + fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), + sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), + watchdogTermination: WatchdogTerminationMonitor? = nil + ) -> RUMScopeDependencies { + return RUMScopeDependencies( + featureScope: featureScope, + rumApplicationID: rumApplicationID, + sessionSampler: sessionSampler, + trackBackgroundEvents: trackBackgroundEvents, + trackFrustrations: trackFrustrations, + firstPartyHosts: firstPartyHosts, + eventBuilder: eventBuilder, + rumUUIDGenerator: rumUUIDGenerator, + backtraceReporter: backtraceReporter, + ciTest: ciTest, + syntheticsTest: syntheticsTest, + vitalsReaders: vitalsReaders, + onSessionStart: onSessionStart, + viewCache: viewCache, + fatalErrorContext: fatalErrorContext, + sessionEndedMetric: sessionEndedMetric, + watchdogTermination: watchdogTermination + ) + } + + /// Creates new instance of `RUMScopeDependencies` by replacing individual dependencies. + func replacing( + rumApplicationID: String? = nil, + sessionSampler: Sampler? = nil, + trackBackgroundEvents: Bool? = nil, + trackFrustrations: Bool? = nil, + firstPartyHosts: FirstPartyHosts? = nil, + eventBuilder: RUMEventBuilder? = nil, + rumUUIDGenerator: RUMUUIDGenerator? = nil, + backtraceReporter: BacktraceReporting? = nil, + ciTest: RUMCITest? = nil, + syntheticsTest: RUMSyntheticsTest? = nil, + vitalsReaders: VitalsReaders? = nil, + onSessionStart: RUM.SessionListener? = nil, + viewCache: ViewCache? = nil, + fatalErrorContext: FatalErrorContextNotifying? = nil, + sessionEndedMetric: SessionEndedMetricController? = nil, + watchdogTermination: WatchdogTerminationMonitor? = nil + ) -> RUMScopeDependencies { + return RUMScopeDependencies( + featureScope: self.featureScope, + rumApplicationID: rumApplicationID ?? self.rumApplicationID, + sessionSampler: sessionSampler ?? self.sessionSampler, + trackBackgroundEvents: trackBackgroundEvents ?? self.trackBackgroundEvents, + trackFrustrations: trackFrustrations ?? self.trackFrustrations, + firstPartyHosts: firstPartyHosts ?? self.firstPartyHosts, + eventBuilder: eventBuilder ?? self.eventBuilder, + rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, + backtraceReporter: backtraceReporter ?? self.backtraceReporter, + ciTest: ciTest ?? self.ciTest, + syntheticsTest: syntheticsTest ?? self.syntheticsTest, + vitalsReaders: vitalsReaders ?? self.vitalsReaders, + onSessionStart: onSessionStart ?? self.onSessionStart, + viewCache: viewCache ?? self.viewCache, + fatalErrorContext: fatalErrorContext ?? self.fatalErrorContext, + sessionEndedMetric: sessionEndedMetric ?? self.sessionEndedMetric, + watchdogTermination: watchdogTermination + ) + } +} + +extension RUMApplicationScope { + static func mockAny() -> RUMApplicationScope { + return RUMApplicationScope(dependencies: .mockAny()) + } +} + +extension RUMSessionScope { + static func mockAny() -> RUMSessionScope { + return mockWith() + } + + // swiftlint:disable function_default_parameter_at_end + static func mockWith( + isInitialSession: Bool = .mockAny(), + parent: RUMContextProvider = RUMContextProviderMock(), + startTime: Date = .mockAny(), + startPrecondition: RUMSessionPrecondition? = .userAppLaunch, + context: DatadogContext = .mockAny(), + dependencies: RUMScopeDependencies = .mockAny(), + hasReplay: Bool? = .mockAny() + ) -> RUMSessionScope { + return RUMSessionScope( + isInitialSession: isInitialSession, + parent: parent, + startTime: startTime, + startPrecondition: startPrecondition, + context: context, + dependencies: dependencies + ) + } + // swiftlint:enable function_default_parameter_at_end +} + +private let mockWindow = UIWindow(frame: .zero) + +func createMockViewInWindow() -> UIViewController { + let viewController = UIViewController() + mockWindow.rootViewController = viewController + mockWindow.makeKeyAndVisible() + return viewController +} + +/// Creates an instance of `UIViewController` subclass with a given name. +func createMockView(viewControllerClassName: String) -> UIViewController { + var theClass: AnyClass! // swiftlint:disable:this implicitly_unwrapped_optional + + if let existingClass = objc_lookUpClass(viewControllerClassName) { + theClass = existingClass + } else { + let newClass: AnyClass = objc_allocateClassPair(UIViewController.classForCoder(), viewControllerClassName, 0)! + objc_registerClassPair(newClass) + theClass = newClass + } + + let viewController = UIViewController() + object_setClass(viewController, theClass) + mockWindow.rootViewController = viewController + mockWindow.makeKeyAndVisible() + return viewController +} + +extension RUMViewScope { + static func mockAny() -> RUMViewScope { + return mockWith() + } + + static func randomTimings() -> [String: Int64] { + var timings: [String: Int64] = [:] + (0..<10).forEach { index in timings["timing\(index)"] = .mockRandom() } + return timings + } + + static func mockWith( + isInitialView: Bool = false, + parent: RUMContextProvider = RUMContextProviderMock(), + dependencies: RUMScopeDependencies = .mockAny(), + identity: ViewIdentifier = .mockViewIdentifier(), + path: String = .mockAny(), + name: String = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + customTimings: [String: Int64] = randomTimings(), + startTime: Date = .mockAny(), + serverTimeOffset: TimeInterval = .zero + ) -> RUMViewScope { + return RUMViewScope( + isInitialView: isInitialView, + parent: parent, + dependencies: dependencies, + identity: identity, + path: path, + name: name, + attributes: attributes, + customTimings: customTimings, + startTime: startTime, + serverTimeOffset: serverTimeOffset + ) + } +} + +extension RUMResourceScope { + static func mockWith( + context: RUMContext, + dependencies: RUMScopeDependencies, + resourceKey: String = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + startTime: Date = .mockAny(), + serverTimeOffset: TimeInterval = .zero, + url: String = .mockAny(), + httpMethod: RUMMethod = .mockAny(), + isFirstPartyResource: Bool? = nil, + resourceKindBasedOnRequest: RUMResourceType? = nil, + spanContext: RUMSpanContext? = .mockAny(), + onResourceEventSent: @escaping () -> Void = {}, + onErrorEventSent: @escaping () -> Void = {} + ) -> RUMResourceScope { + return RUMResourceScope( + context: context, + dependencies: dependencies, + resourceKey: resourceKey, + attributes: attributes, + startTime: startTime, + serverTimeOffset: serverTimeOffset, + url: url, + httpMethod: httpMethod, + resourceKindBasedOnRequest: resourceKindBasedOnRequest, + spanContext: spanContext, + onResourceEventSent: onResourceEventSent, + onErrorEventSent: onErrorEventSent + ) + } +} + +extension RUMUserActionScope { + // swiftlint:disable function_default_parameter_at_end + static func mockWith( + parent: RUMContextProvider, + dependencies: RUMScopeDependencies = .mockAny(), + name: String = .mockAny(), + actionType: RUMActionType = [.tap, .scroll, .swipe, .custom].randomElement()!, + attributes: [AttributeKey: AttributeValue] = [:], + startTime: Date = .mockAny(), + serverTimeOffset: TimeInterval = .zero, + isContinuous: Bool = .mockAny(), + instrumentation: InstrumentationType = .manual, + onActionEventSent: @escaping (RUMActionEvent) -> Void = { _ in } + ) -> RUMUserActionScope { + return RUMUserActionScope( + parent: parent, + dependencies: dependencies, + name: name, + actionType: actionType, + attributes: attributes, + startTime: startTime, + serverTimeOffset: serverTimeOffset, + isContinuous: isContinuous, + instrumentation: instrumentation, + onActionEventSent: onActionEventSent + ) + } + // swiftlint:enable function_default_parameter_at_end +} + +class RUMContextProviderMock: RUMContextProvider { + init(context: RUMContext = .mockAny()) { + self.context = context + } + + var context: RUMContext +} + +// MARK: - Auto Instrumentation Mocks + +class RUMCommandSubscriberMock: RUMCommandSubscriber { + var onCommandReceived: ((RUMCommand) -> Void)? + var receivedCommands: [RUMCommand] = [] + var lastReceivedCommand: RUMCommand? { receivedCommands.last } + + func process(command: RUMCommand) { + receivedCommands.append(command) + onCommandReceived?(command) + } +} + +class UIKitRUMViewsPredicateMock: UIKitRUMViewsPredicate { + var resultByViewController: [UIViewController: RUMView] = [:] + var result: RUMView? + + init(result: RUMView? = nil) { + self.result = result + } + + func rumView(for viewController: UIViewController) -> RUMView? { + return resultByViewController[viewController] ?? result + } +} + +class UIKitRUMViewsHandlerMock: UIViewControllerHandler { + var onSubscribe: ((RUMCommandSubscriber) -> Void)? + var notifyViewDidAppear: ((UIViewController, Bool) -> Void)? + var notifyViewDidDisappear: ((UIViewController, Bool) -> Void)? + + func publish(to subscriber: RUMCommandSubscriber) { + onSubscribe?(subscriber) + } + + func notify_viewDidAppear(viewController: UIViewController, animated: Bool) { + notifyViewDidAppear?(viewController, animated) + } + + func notify_viewDidDisappear(viewController: UIViewController, animated: Bool) { + notifyViewDidDisappear?(viewController, animated) + } +} + +#if os(tvOS) +typealias UIKitRUMActionsPredicateMock = UIPressRUMActionsPredicateMock +#else +typealias UIKitRUMActionsPredicateMock = UITouchRUMActionsPredicateMock +#endif + +class UITouchRUMActionsPredicateMock: UITouchRUMActionsPredicate { + var resultByView: [UIView: RUMAction] = [:] + var result: RUMAction? + + init(result: RUMAction? = nil) { + self.result = result + } + + func rumAction(targetView: UIView) -> RUMAction? { + return resultByView[targetView] ?? result + } +} + +class UIPressRUMActionsPredicateMock: UIPressRUMActionsPredicate { + var resultByView: [UIView: RUMAction] = [:] + var result: RUMAction? + + init(result: RUMAction? = nil) { + self.result = result + } + + func rumAction(press type: UIPress.PressType, targetView: UIView) -> RUMAction? { + return resultByView[targetView] ?? result + } +} + +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) + } + + 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 { + var vitalData: Double? + + func readVitalData() -> Double? { + return vitalData + } +} + +class ContinuousVitalReaderMock: ContinuousVitalReader { + var vitalInfo = VitalInfo() { + didSet { + publishers.forEach { + $0.publishAsync(vitalInfo) + } + } + } + var publishers = [VitalPublisher]() + + func register(_ valuePublisher: VitalPublisher) { + publishers.append(valuePublisher) + } + + func unregister(_ valuePublisher: VitalPublisher) { + publishers.removeAll { existingPublisher in + return existingPublisher === valuePublisher + } + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.swift b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.swift new file mode 100644 index 0000000000..f6f087f923 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.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. + */ + +#if canImport(CoreTelephony) +import CoreTelephony + +/* +A collection of mocks for different `CoreTelephony` types. +It follows the mocking conventions described in `FoundationMocks.swift`. + */ + +class CTCarrierMock: CTCarrier { + private let _carrierName: String? + private let _isoCountryCode: String? + private let _allowsVOIP: Bool + + init(carrierName: String, isoCountryCode: String, allowsVOIP: Bool) { + _carrierName = carrierName + _isoCountryCode = isoCountryCode + _allowsVOIP = allowsVOIP + } + + override var carrierName: String? { _carrierName } + override var isoCountryCode: String? { _isoCountryCode } + override var allowsVOIP: Bool { _allowsVOIP } +} + +class CTTelephonyNetworkInfoMock: CTTelephonyNetworkInfo { + private var _serviceCurrentRadioAccessTechnology: [String: String]? + private var _serviceSubscriberCellularProviders: [String: CTCarrier]? + + init( + serviceCurrentRadioAccessTechnology: [String: String], + serviceSubscriberCellularProviders: [String: CTCarrier] + ) { + _serviceCurrentRadioAccessTechnology = serviceCurrentRadioAccessTechnology + _serviceSubscriberCellularProviders = serviceSubscriberCellularProviders + } + + func changeCarrier( + newCarrierName: String, + newISOCountryCode: String, + newAllowsVOIP: Bool, + newRadioAccessTechnology: String + ) { + _serviceCurrentRadioAccessTechnology = [ + "000001": newRadioAccessTechnology + ] + _serviceSubscriberCellularProviders = [ + "000001": CTCarrierMock(carrierName: newCarrierName, isoCountryCode: newISOCountryCode, allowsVOIP: newAllowsVOIP) + ] + + serviceSubscriberCellularProvidersDidUpdateNotifier?("000001") + } + + override var serviceCurrentRadioAccessTechnology: [String: String]? { _serviceCurrentRadioAccessTechnology } + override var serviceSubscriberCellularProviders: [String: CTCarrier]? { _serviceSubscriberCellularProviders } +} + +#endif diff --git a/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/UIKitMocks.swift b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/UIKitMocks.swift new file mode 100644 index 0000000000..1fd4bf2a00 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/UIKitMocks.swift @@ -0,0 +1,133 @@ +/* + * 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 TestUtilities + +/* +A collection of mocks for different `UIKit` types. +It follows the mocking conventions described in `FoundationMocks.swift`. + */ + +#if !os(tvOS) +extension UIDevice.BatteryState { + static func mockAny() -> UIDevice.BatteryState { + return .full + } +} +#endif + +extension UIEvent { + static func mockAnyTouch() -> UIEvent { + return .mockWith(touches: [.mockAny()]) + } + + static func mockAnyPress() -> UIEvent { + return .mockWith(touches: [.mockAny()]) + } + + static func mockWith(touch: UITouch) -> UIEvent { + return UIEventMock(allTouches: [touch]) + } + + static func mockWith(touches: Set?) -> UIEvent { + return UIEventMock(allTouches: touches) + } + + static func mockWith(press: UIPress) -> UIPressesEvent { + return UIPressesEventMock(allPresses: [press]) + } + + static func mockWith(presses: Set) -> UIPressesEvent { + return UIPressesEventMock(allPresses: presses) + } +} + +private class UIEventMock: UIEvent { + private let _allTouches: Set? + + fileprivate init(allTouches: Set?) { + _allTouches = allTouches + } + + override var allTouches: Set? { _allTouches } +} + +private class UIPressesEventMock: UIPressesEvent { + private let _allPresses: Set + + fileprivate init(allPresses: Set = []) { + _allPresses = allPresses + } + + override var allPresses: Set { _allPresses } +} + +extension UITouch { + static func mockAny() -> UITouch { + return mockWith(view: UIView()) + } + + static func mockWith( + phase: UITouch.Phase = .ended, + view: UIView? = .init() + ) -> UITouch { + return UITouchMock(phase: phase, view: view) + } +} + +extension UIPress { + static func mockAny() -> UIPress { + return mockWith(type: .select, view: UIView()) + } + + static func mockWith( + phase: UIPress.Phase = .ended, + type: UIPress.PressType = .select, + view: UIView? = .init() + ) -> UIPress { + return UIPressMock(phase: phase, type: type, view: view) + } +} + +private class UITouchMock: UITouch { + private let _phase: UITouch.Phase + private let _view: UIView? + + fileprivate init(phase: UITouch.Phase, view: UIView?) { + _phase = phase + _view = view + } + + override var phase: UITouch.Phase { _phase } + override var view: UIView? { _view } +} + +private class UIPressMock: UIPress { + private let _phase: UIPress.Phase + private let _type: UIPress.PressType + private let _view: UIView? + + fileprivate init(phase: UIPress.Phase, type: UIPress.PressType, view: UIView?) { + _phase = phase + _type = type + _view = view + } + + override var phase: UIPress.Phase { _phase } + override var type: UIPress.PressType { _type } + override var responder: UIResponder? { _view } +} + +extension UIApplication.State: AnyMockable, RandomMockable { + public static func mockAny() -> UIApplication.State { + return .active + } + + public static func mockRandom() -> UIApplication.State { + return [.active, .inactive, .background].randomElement()! + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift new file mode 100644 index 0000000000..0382373eff --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/SystemFrameworks/WebKitMocks.swift @@ -0,0 +1,57 @@ +/* + * 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 DatadogWebViewTracking + +#if !os(tvOS) +import WebKit + +final class WKUserContentControllerMock: WKUserContentController { + private var handlers: [String: WKScriptMessageHandler] = [:] + + override func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) { + handlers[name] = scriptMessageHandler + } + + override func removeScriptMessageHandler(forName name: String) { + handlers[name] = nil + } + + func send(body: Any, from webView: WKWebView? = nil) { + let handler = handlers[DDScriptMessageHandler.name] + let message = WKScriptMessageMock(body: body, name: DDScriptMessageHandler.name, webView: webView) + handler?.userContentController(self, didReceive: message) + } + + func scriptMessageHandler(forName name: String) -> WKScriptMessageHandler? { + handlers[name] + } + + func flush() { + let handler = handlers[DDScriptMessageHandler.name] as? DDScriptMessageHandler + handler?.flush() + } +} + +private final class WKScriptMessageMock: WKScriptMessage { + private let _body: Any + private let _name: String + private weak var _webView: WKWebView? + + init(body: Any, name: String, webView: WKWebView? = nil) { + _body = body + _name = name + _webView = webView + } + + override var body: Any { _body } + override var name: String { _name } + override weak var webView: WKWebView? { _webView } +} + +#endif diff --git a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift new file mode 100644 index 0000000000..496204d1e4 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift @@ -0,0 +1,176 @@ +/* + * 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 TestUtilities +import DatadogInternal + +@testable import DatadogTrace + +extension DatadogCoreProxy { + func waitAndReturnSpanMatchers(file: StaticString = #file, line: UInt = #line) throws -> [SpanMatcher] { + return try waitAndReturnEventsData(ofFeature: TraceFeature.name) + .map { eventData in try SpanMatcher.fromJSONObjectData(eventData) } + } + + func waitAndReturnSpanEvents(file: StaticString = #file, line: UInt = #line) -> [SpanEvent] { + return waitAndReturnEvents(ofFeature: TraceFeature.name, ofType: SpanEventsEnvelope.self) + .map { envelope in + precondition(envelope.spans.count == 1, "Only expect one `SpanEvent` per envelope") + return envelope.spans[0] + } + } +} + +// MARK: - Span Mocks + +internal struct NOPSpanWriteContext: SpanWriteContext { + func spanWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) {} +} + +extension DDSpan { + static func mockAny(in core: DatadogCoreProtocol) -> DDSpan { + return mockWith(core: core) + } + + static func mockWith( + tracer: DatadogTracer, + context: DDSpanContext = .mockAny(), + operationName: String = .mockAny(), + startTime: Date = .mockAny(), + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() + ) -> DDSpan { + return DDSpan( + tracer: tracer, + context: context, + operationName: operationName, + startTime: startTime, + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter + ) + } + + static func mockWith( + core: DatadogCoreProtocol, + context: DDSpanContext = .mockAny(), + operationName: String = .mockAny(), + startTime: Date = .mockAny(), + tags: [String: Encodable] = [:], + eventBuilder: SpanEventBuilder = .mockAny(), + eventWriter: SpanWriteContext = NOPSpanWriteContext() + ) -> DDSpan { + return DDSpan( + tracer: .mockAny(in: core), + context: context, + operationName: operationName, + startTime: startTime, + tags: tags, + eventBuilder: eventBuilder, + eventWriter: eventWriter + ) + } +} + +extension DDSpanContext { + static func mockAny() -> DDSpanContext { + return mockWith() + } + + static func mockWith( + traceID: TraceID = .mockAny(), + spanID: SpanID = .mockAny(), + parentSpanID: SpanID? = .mockAny(), + baggageItems: BaggageItems = .mockAny(), + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() + ) -> DDSpanContext { + return DDSpanContext( + traceID: traceID, + spanID: spanID, + parentSpanID: parentSpanID, + baggageItems: baggageItems, + sampleRate: sampleRate, + isKept: isKept + ) + } +} + +extension BaggageItems { + static func mockAny() -> BaggageItems { + return BaggageItems() + } +} + +// MARK: - Component Mocks + +extension DatadogTracer { + static func mockAny(in core: DatadogCoreProtocol) -> DatadogTracer { + return mockWith(core: core) + } + + static func mockWith( + core: DatadogCoreProtocol, + localTraceSampler: Sampler = .mockKeepAll(), + tags: [String: Encodable] = [:], + traceIDGenerator: TraceIDGenerator = DefaultTraceIDGenerator(), + spanIDGenerator: SpanIDGenerator = DefaultSpanIDGenerator(), + dateProvider: DateProvider = SystemDateProvider(), + spanEventBuilder: SpanEventBuilder = .mockAny(), + loggingIntegration: TracingWithLoggingIntegration = .mockAny() + ) -> DatadogTracer { + return DatadogTracer( + core: core, + localTraceSampler: localTraceSampler, + tags: tags, + traceIDGenerator: traceIDGenerator, + spanIDGenerator: spanIDGenerator, + dateProvider: dateProvider, + loggingIntegration: loggingIntegration, + spanEventBuilder: spanEventBuilder + ) + } +} + +extension TracingWithLoggingIntegration { + static func mockAny() -> TracingWithLoggingIntegration { + return TracingWithLoggingIntegration( + core: NOPDatadogCore(), + service: .mockAny(), + networkInfoEnabled: .mockAny() + ) + } +} + +extension ContextMessageReceiver { + static func mockAny() -> ContextMessageReceiver { + return ContextMessageReceiver() + } +} + +extension SpanEventBuilder { + static func mockAny() -> SpanEventBuilder { + return mockWith() + } + + static func mockWith( + service: String = .mockAny(), + networkInfoEnabled: Bool = false, + eventsMapper: SpanEventMapper? = nil, + bundleWithRUM: Bool = false, + telemetry: Telemetry = NOPTelemetry() + ) -> SpanEventBuilder { + return SpanEventBuilder( + service: service, + networkInfoEnabled: networkInfoEnabled, + eventsMapper: eventsMapper, + bundleWithRUM: bundleWithRUM, + telemetry: telemetry + ) + } +} diff --git a/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift b/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift new file mode 100644 index 0000000000..5d8d0c4223 --- /dev/null +++ b/DatadogCore/Tests/Datadog/OpenTracing/OTSpanTests.swift @@ -0,0 +1,242 @@ +/* + * 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 DatadogTrace + +private class MockSpan: OTSpan { + var context: OTSpanContext = DDNoopGlobals.context + func tracer() -> OTTracer { DDNoopGlobals.tracer } + func setOperationName(_ operationName: String) {} + func setTag(key: String, value: Encodable) {} + func setBaggageItem(key: String, value: String) {} + func baggageItem(withKey key: String) -> String? { nil } + func setActive() -> OTSpan { self } + func finish(at time: Date) {} + + var logs: [[String: Encodable]] = [] + + func log(fields: [String: Encodable], timestamp: Date) { + logs.append(fields) + } +} + +private extension Dictionary where Key == String, Value == Encodable { + func otEvent() throws -> String { + try XCTUnwrap(self[OTLogFields.event] as? String) + } + + func otKind() throws -> String { + try XCTUnwrap(self[OTLogFields.errorKind] as? String) + } + + func otMessage() throws -> String { + try XCTUnwrap(self[OTLogFields.message] as? String) + } + + func otStack() throws -> String { + try XCTUnwrap(self[OTLogFields.stack] as? String) + } +} + +class OTSpanTests: XCTestCase { + // MARK: - Test Error Conveniences + + func testWhenSettingErrorFromSwiftError_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + #sourceLocation(file: "File.swift", line: 42) + span.setError(ErrorMock("swift error description")) + #sourceLocation() + span.finish() + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "ErrorMock") + XCTAssertEqual(try span.logs[0].otMessage(), "swift error description") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + \(moduleName())/File.swift:42 + swift error description + """ + ) + } + + func testWhenSettingErrorFromSwiftErrorWithFileAndLine_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + span.setError(ErrorMock("swift error description"), file: "File.swift", line: 42) + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "ErrorMock") + XCTAssertEqual(try span.logs[0].otMessage(), "swift error description") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + File.swift:42 + swift error description + """ + ) + } + + func testWhenSettingErrorFromNSError_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + #sourceLocation(file: "File.swift", line: 42) + span.setError( + NSError( + domain: "DDSpan", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "ns error description"] + ) + ) + #sourceLocation() + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "DDSpan - 1") + XCTAssertEqual(try span.logs[0].otMessage(), "ns error description") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + \(moduleName())/File.swift:42 + Error Domain=DDSpan Code=1 "ns error description" UserInfo={NSLocalizedDescription=ns error description} + """ + ) + } + + func testWhenSettingErrorFromArguments_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + #sourceLocation(file: "File.swift", line: 42) + span.setError(kind: "custom kind", message: "DDSpan Error") + #sourceLocation() + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "custom kind") + XCTAssertEqual(try span.logs[0].otMessage(), "DDSpan Error") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + \(moduleName())/File.swift:42 + """ + ) + } + + func testWhenSettingErrorFromArgumentsWithStack_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + let stack = """ + Thread 0 Crashed: + 0 app 0x0000000102bc0d8c 0x102bb8000 + 36236 + 1 UIKitCore 0x00000001b513d9ac 0x1b4739000 + 10504620 + """ + #sourceLocation(file: "File.swift", line: 42) + span.setError(kind: "custom kind", message: "custom message", stack: stack) + #sourceLocation() + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "custom kind") + XCTAssertEqual(try span.logs[0].otMessage(), "custom message") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + \(moduleName())/File.swift:42 + Thread 0 Crashed: + 0 app 0x0000000102bc0d8c 0x102bb8000 + 36236 + 1 UIKitCore 0x00000001b513d9ac 0x1b4739000 + 10504620 + """ + ) + } + + func testWhenSettingErrorFromArgumentsWithFileAndLine_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + span.setError(kind: "custom kind", message: "custom message", file: "File.swift", line: 42) + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "custom kind") + XCTAssertEqual(try span.logs[0].otMessage(), "custom message") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + File.swift:42 + """ + ) + } + + func testWhenSettingErrorWithEmptyFileLineAndStack_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + span.setError(ErrorMock("swift error description"), file: "", line: 0) + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "ErrorMock") + XCTAssertEqual(try span.logs[0].otMessage(), "swift error description") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + swift error description + """ + ) + } + + func testWhenSettingErrorWithEmptyFileLineAndNonEmptyStack_itLogsErrorFields() throws { + // Given + let span = MockSpan() + + // When + span.setError(ErrorMock("the stack"), file: "", line: 0) + + // Then + XCTAssertEqual(span.logs.count, 1) + XCTAssertEqual(span.logs[0].count, 4) + XCTAssertEqual(try span.logs[0].otEvent(), "error") + XCTAssertEqual(try span.logs[0].otKind(), "ErrorMock") + XCTAssertEqual(try span.logs[0].otMessage(), "the stack") + XCTAssertEqual( + try span.logs[0].otStack(), + """ + the stack + """ + ) + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/Casting+RUM.swift b/DatadogCore/Tests/Datadog/RUM/Casting+RUM.swift new file mode 100644 index 0000000000..3e5718729c --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/Casting+RUM.swift @@ -0,0 +1,11 @@ +/* + * 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. + */ + +@testable import DatadogRUM + +internal extension RUMMonitorProtocol { + var dd: Monitor { self as! Monitor } +} diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift new file mode 100644 index 0000000000..6de0000be1 --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -0,0 +1,1477 @@ +/* + * 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 +@testable import DatadogRUM +@testable import DatadogCrashReporting + +class CrashReportReceiverTests: XCTestCase { + private let featureScope = FeatureScopeMock() + + func testReceiveCrashEvent() throws { + // Given + let receiver: CrashReportReceiver = .mockWith(featureScope: featureScope) + + // When + let message: FeatureMessage = .baggage( + key: CrashReportReceiver.MessageKeys.crash, + value: MessageBusSender.Crash( + report: DDCrashReport.mockAny(), + context: CrashContext.mockWith(lastRUMViewEvent: nil) + ) + ) + let result = receiver.receive(message: message, from: NOPDatadogCore()) + + // Then + XCTAssertTrue(result, "It must accept the message") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1, "It should send error event") + } + + func testReceiveCrashAndViewEvent() throws { + // Given + let receiver: CrashReportReceiver = .mockWith(featureScope: featureScope) + let lastRUMViewEvent: RUMViewEvent = .mockRandom() + + // When + let message: FeatureMessage = .baggage( + key: CrashReportReceiver.MessageKeys.crash, + value: MessageBusSender.Crash( + report: DDCrashReport.mockWith(date: Date()), + context: CrashContext.mockWith( + lastRUMViewEvent: AnyCodable(lastRUMViewEvent) + ) + ) + ) + let result = receiver.receive(message: message, from: NOPDatadogCore()) + + // Then + XCTAssertTrue(result, "It must accept the message") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1, "It should send error event") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1, "It should send view event") + } + + // MARK: - Testing Conditional Uploads + + func testGivenCrashDuringRUMSessionWithActiveViewCollectedLessThan4HoursAgo_whenSending_itSendsBothRUMErrorAndRUMViewEvent() throws { + let secondsIn4Hours: TimeInterval = 4 * 60 * 60 + + // Given + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let crashDate: Date = currentDate.secondsAgo(.random(in: 0.. Void) throws -> (utilization: Double, duration: Double) { + let startTime = CFAbsoluteTimeGetCurrent() + let startUtilization = try XCTUnwrap(cpuReader.readVitalData()) + + block() + + let endUtilization = try XCTUnwrap(cpuReader.readVitalData()) + let duration = CFAbsoluteTimeGetCurrent() - startTime + + let utilizedTicks = endUtilization - startUtilization + + return (utilizedTicks / duration, duration) + } +} + +fileprivate func heavyLoad() { + // cpuTicksResolution is measured by trial&error. + // most of the time `readVitalData()` returns incremented data after 0.01sec. + // however, sometimes it returns the same value for 1.0sec. + // looking at the source code, iOS should update cpu ticks at + // every thread scheduling and/or system->user/user->system mode changes in CPU. + // but empirically, it gets stuck for 1.0sec randomly. + let worstCaseCPUTicksResolution: TimeInterval = 1.0 + let startDate = Date() + + while Date().timeIntervalSince(startDate) <= worstCaseCPUTicksResolution { + for _ in 0...100_000 { + let random = Double.random(in: Double.leastNonzeroMagnitude...Double.greatestFiniteMagnitude) + _ = tan(random).squareRoot() + } + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift new file mode 100644 index 0000000000..aaafdfc184 --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift @@ -0,0 +1,124 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogRUM + +class VitalInfoSamplerTests: XCTestCase { + func testItDoesSamplePeriodically() { + let mockCPUReader = SamplingBasedVitalReaderMock() + let mockMemoryReader = SamplingBasedVitalReaderMock() + let mockRefreshRateReader = ContinuousVitalReaderMock() + + let sampler = VitalInfoSampler( + cpuReader: mockCPUReader, + memoryReader: mockMemoryReader, + refreshRateReader: mockRefreshRateReader, + frequency: 0.1 + ) + + mockCPUReader.vitalData = 123.0 + mockMemoryReader.vitalData = 321.0 + mockRefreshRateReader.vitalInfo = { + var info = VitalInfo() + info.addSample(60.0) + return info + }() + + let samplingExpectation = expectation(description: "sampling expectation") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + samplingExpectation.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in + XCTAssertEqual(sampler.cpu.meanValue, 123.0) + XCTAssertGreaterThan(sampler.cpu.sampleCount, 1) + XCTAssertEqual(sampler.memory.meanValue, 321.0) + XCTAssertGreaterThan(sampler.memory.sampleCount, 1) + XCTAssertEqual(sampler.refreshRate.meanValue, 60.0) + } + } + + func testItDoesInitialSample() { + let mockCPUReader = SamplingBasedVitalReaderMock() + let mockMemoryReader = SamplingBasedVitalReaderMock() + + let sampler = VitalInfoSampler( + cpuReader: mockCPUReader, + memoryReader: mockMemoryReader, + refreshRateReader: ContinuousVitalReaderMock(), + frequency: 0.5 + ) + + mockCPUReader.vitalData = 123.0 + mockMemoryReader.vitalData = 321.0 + + let samplingExpectation = expectation(description: "sampling expectation") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + samplingExpectation.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in + XCTAssertEqual(sampler.cpu.sampleCount, 1) + XCTAssertEqual(sampler.memory.sampleCount, 1) + } + } + + func testItDoesSamplePeriodicallyWithLowFrequency() { + let mockCPUReader = SamplingBasedVitalReaderMock() + let mockMemoryReader = SamplingBasedVitalReaderMock() + + let sampler = VitalInfoSampler( + cpuReader: mockCPUReader, + memoryReader: mockMemoryReader, + refreshRateReader: ContinuousVitalReaderMock(), + frequency: 0.5 + ) + + mockCPUReader.vitalData = 123.0 + mockMemoryReader.vitalData = 321.0 + + let samplingExpectation = expectation(description: "sampling expectation") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.6) { + samplingExpectation.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in + // 2 samples, initial sample and one periodic sample + XCTAssertEqual(sampler.cpu.sampleCount, 2) + XCTAssertEqual(sampler.memory.sampleCount, 2) + } + } + + func testItSamplesDataFromBackgroundThreads() { + // swiftlint:disable implicitly_unwrapped_optional + var sampler: VitalInfoSampler! + DispatchQueue.global().sync { + // in real-world scenarios, sampling will be started from background threads + sampler = VitalInfoSampler( + cpuReader: VitalCPUReader(notificationCenter: .default), + memoryReader: VitalMemoryReader(), + refreshRateReader: VitalRefreshRateReader(notificationCenter: .default), + frequency: 0.1 + ) + } + + let samplingExpectation = expectation(description: "sampling expectation") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + samplingExpectation.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in + XCTAssertGreaterThan(sampler.cpu.meanValue!, 0.0) + XCTAssertGreaterThan(sampler.cpu.sampleCount, 1) + XCTAssertGreaterThan(sampler.memory.meanValue!, 0.0) + XCTAssertGreaterThan(sampler.memory.sampleCount, 1) + XCTAssertGreaterThan(sampler.refreshRate.meanValue!, 0.0) + XCTAssertGreaterThan(sampler.refreshRate.sampleCount, 1) + } + // swiftlint:enable implicitly_unwrapped_optional + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoTests.swift new file mode 100644 index 0000000000..8c7436536c --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoTests.swift @@ -0,0 +1,79 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogRUM + +class VitalInfoTest: XCTestCase { + func testItUpdatesVitalInfoOnFirstValue() { + let randomValue = Double.random(in: -65_536.0...65_536.0) + var testedInfo = VitalInfo() + + XCTAssertEqual(testedInfo.sampleCount, 0) + XCTAssertNil(testedInfo.minValue) + XCTAssertNil(testedInfo.maxValue) + XCTAssertNil(testedInfo.meanValue) + XCTAssertNil(testedInfo.greatestDiff) + + // When + testedInfo.addSample(randomValue) + + // Then + XCTAssertEqual(testedInfo.minValue, randomValue) + XCTAssertEqual(testedInfo.maxValue, randomValue) + XCTAssertEqual(testedInfo.meanValue, randomValue) + XCTAssertEqual(testedInfo.sampleCount, 1) + XCTAssertEqual(testedInfo.greatestDiff, 0) + } + + func testItUpdatesVitalInfoOnMultipleValue() { + let randomValues = (0..<3).map { _ in Double.random(in: -65_536.0...65_536.0) } + var testedInfo = VitalInfo() + + // When + testedInfo.addSample(randomValues[0]) + // Then + XCTAssertEqual(testedInfo.minValue, randomValues[0]) + XCTAssertEqual(testedInfo.maxValue, randomValues[0]) + XCTAssertEqual(testedInfo.meanValue, randomValues[0]) + XCTAssertEqual(testedInfo.sampleCount, 1) + XCTAssertEqual(testedInfo.greatestDiff, 0) + + // When + testedInfo.addSample(randomValues[1]) + // Then + XCTAssertEqual(testedInfo.minValue, randomValues[0...1].min()) + XCTAssertEqual(testedInfo.maxValue, randomValues[0...1].max()) + XCTAssertEqual(testedInfo.meanValue, randomValues[0...1].reduce(0, +) / 2.0) + XCTAssertEqual(testedInfo.sampleCount, 2) + XCTAssertEqual(testedInfo.greatestDiff, randomValues[0...1].max()! - randomValues[0...1].min()!) + + // When + testedInfo.addSample(randomValues[2]) + // Then + XCTAssertEqual(testedInfo.minValue, randomValues[0...2].min()) + XCTAssertEqual(testedInfo.maxValue, randomValues[0...2].max()) + XCTAssertEqual(testedInfo.meanValue, randomValues[0...2].reduce(0, +) / 3.0) + XCTAssertEqual(testedInfo.sampleCount, 3) + XCTAssertEqual(testedInfo.greatestDiff, randomValues[0...2].max()! - randomValues[0...2].min()!) + } + + func testItScalesDown() { + let randomValue = Double.random(in: -65_536.0...65_536.0) + var randomInfo = VitalInfo() + randomInfo.addSample(randomValue) + + // When + let testedInfo = randomInfo.scaledDown(by: 2.0) + + // Then + XCTAssertEqual(testedInfo.minValue, randomValue / 2.0) + XCTAssertEqual(testedInfo.maxValue, randomValue / 2.0) + XCTAssertEqual(testedInfo.meanValue, randomValue / 2.0) + XCTAssertEqual(testedInfo.sampleCount, 1) + XCTAssertEqual(testedInfo.greatestDiff, 0) + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalMemoryReaderTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalMemoryReaderTests.swift new file mode 100644 index 0000000000..5d7d94f2de --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalMemoryReaderTests.swift @@ -0,0 +1,96 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +@testable import DatadogRUM + +private class Allocation { + fileprivate let numberOfBytes: Int + private var pointer: UnsafeMutablePointer! // swiftlint:disable:this implicitly_unwrapped_optional + + init(numberOfBytes: Int) { + self.numberOfBytes = numberOfBytes + } + + func allocate() { + pointer = UnsafeMutablePointer.allocate(capacity: numberOfBytes) + pointer.initialize(repeating: 0, count: numberOfBytes) + } + + func deallocate() { + pointer.deallocate() + } +} + +internal class VitalMemoryReaderTest: XCTestCase { + private let allocation = Allocation( + numberOfBytes: 8 * 1_024 * 1_024 // 8MB is significantly more than any other allocation in tests process + ) + + func testReadMemory() throws { + let reader = VitalMemoryReader() + let result = reader.readVitalData() + XCTAssertNotNil(result) + } + + func testWhenMemoryConsumptionGrows() throws { + // Given + let reader = VitalMemoryReader() + + // When + var deltas: [Double] = [] + + try (0..<20).forEach { _ in // measure mean value to mitigate flakiness + let before = try XCTUnwrap(reader.readVitalData()) + allocation.allocate() + let after = try XCTUnwrap(reader.readVitalData()) + allocation.deallocate() + deltas.append(after - before) + } + + // Then + let meanDelta = deltas.reduce(0.0, +) / Double(deltas.count) + let expectedMeanDelta = Double(allocation.numberOfBytes) * 0.6 // only 60% to mitigate external deallocations in the process + + XCTAssertGreaterThan( + meanDelta, + expectedMeanDelta, + "Mean delta \(toMB(meanDelta))MB is not greater than \(toMB(expectedMeanDelta))MB" + ) + } + + func testWhenMemoryConsumptionShrinks() throws { + // Given + let reader = VitalMemoryReader() + + // When + var deltas: [Double] = [] + + try (0..<20).forEach { _ in // measure mean value to mitigate flakiness + allocation.allocate() + let before = try XCTUnwrap(reader.readVitalData()) + allocation.deallocate() + let after = try XCTUnwrap(reader.readVitalData()) + deltas.append(after - before) + } + + // Then + let meanDelta = deltas.reduce(0.0, +) / Double(deltas.count) + let expectedMeanDelta = Double(allocation.numberOfBytes) * 0.6 // only 60% to mitigate external allocations in the process + + XCTAssertLessThan( + meanDelta, + expectedMeanDelta, + "Mean delta \(toMB(meanDelta))MB is not less than \(toMB(expectedMeanDelta))MB" + ) + } + + // MARK: - Helpers + + private func toMB(_ bytes: Double) -> Double { + return round(bytes / (1_024 * 1_024) * 100) / 100 + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift new file mode 100644 index 0000000000..357c5bc23e --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.swift @@ -0,0 +1,257 @@ +/* + * 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 UIKit +@testable import DatadogRUM + +class VitalRefreshRateReaderTests: XCTestCase { + private let mockNotificationCenter = NotificationCenter() + + func testWhenMainThreadOverheadGoesUp_itMeasuresLowerRefreshRate() throws { + let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) + let targetSamplesCount = 30 + + /// Runs given work on the main thread until `condition` is met, then calls `completion`. + func run(mainThreadWork: @escaping () -> Void, until condition: @escaping () -> Bool, completion: @escaping () -> Void) { + if !condition() { + mainThreadWork() + DispatchQueue.main.async { // schedule to next runloop + run(mainThreadWork: mainThreadWork, until: condition, completion: completion) + } + } else { + completion() + } + } + + /// Records `targetSamplesCount` samples into `measure` by running given work on the main thread. + func record(_ measure: VitalPublisher, mainThreadWork: @escaping () -> Void) { + let completion = expectation(description: "Complete measurement") + reader.register(measure) + + run( + mainThreadWork: mainThreadWork, + until: { measure.currentValue.sampleCount >= targetSamplesCount }, + completion: { + reader.unregister(measure) + completion.fulfill() + } + ) + + let result = XCTWaiter().wait(for: [completion], timeout: 10) + + switch result { + case .completed: + break // all good + case .timedOut: + XCTFail("VitalRefreshRateReader exceededed timeout with \(measure.currentValue.sampleCount)/\(targetSamplesCount) recorded samples") + default: + XCTFail("XCTWaiter unexpected failure: \(result)") + } + } + + // Given + let lowOverhead = { /* no-op */ } // no overhead in succeeding runloop runs + let lowOverheadMeasure = VitalPublisher(initialValue: VitalInfo()) + + var highOverheadRunCount = 0 + let highOverhead = { highOverheadRunCount += 1; Thread.sleep(forTimeInterval: 0.02) } // 0.02 overhead in succeeding runloop runs + let highOverheadMeasure = VitalPublisher(initialValue: VitalInfo()) + + // When + record(lowOverheadMeasure, mainThreadWork: lowOverhead) + record(highOverheadMeasure, mainThreadWork: highOverhead) + + // Then + let expectedHighFPS = try XCTUnwrap(lowOverheadMeasure.currentValue.meanValue) + let expectedLowFPS = try XCTUnwrap(highOverheadMeasure.currentValue.meanValue) + XCTAssertGreaterThan(expectedHighFPS, expectedLowFPS, "It must measure higher FPS for lower main thread overhead (hight overhead run count: \(highOverheadRunCount))") + } + + func testAppStateHandling() { + let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) + let registrar = VitalPublisher(initialValue: VitalInfo()) + + mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil) + mockNotificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) + reader.register(registrar) + + let expectation1 = expectation(description: "async expectation for first observer") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + expectation1.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in } + XCTAssertEqual(registrar.currentValue.sampleCount, 0) + + mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil) + + let expectation2 = expectation(description: "async expectation for second observer") + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + expectation2.fulfill() + } + + waitForExpectations(timeout: 1.0) { _ in } + XCTAssertGreaterThan(registrar.currentValue.sampleCount, 0) + } + + /* Rate representation + * + * 0-------------------16ms------------------32ms----------------48ms + * | 16ms | 16ms | 16ms | + * Skipped + */ + func testFramesPerSecond_given60HzFixedRateDisplay() { + let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) + var frameInfoProvider = FrameInfoProviderMock(maximumDeviceFramesPerSecond: 60) + + // first frame recorded + frameInfoProvider.currentFrameTimestamp = 0 + frameInfoProvider.nextFrameTimestamp = 0.016 + let firstFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertNil(firstFps) + + // second frame recorded + frameInfoProvider.currentFrameTimestamp = 0.016 + frameInfoProvider.nextFrameTimestamp = 0.032 + let secondFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(secondFps, 62.5) // fractional value due to low precision of timestamps + + // third frame recorded + frameInfoProvider.currentFrameTimestamp = 0.048 + let thirdFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(thirdFps, 31.25) // fractional value due to low precision of timestamps + } + + /* Rate representation + * + * 0----------8ms---------16ms--------24ms--------32ms + * | 8ms | 8ms | 8ms | 8ms | + * Skipped + */ + func testFramesPerSecond_given120HzFixedRateDisplay_normalizesTo60Hz() { + 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.008 + frameInfoProvider.nextFrameTimestamp = 0.016 + let secondFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(secondFps, 60) + + // third frame recorded + frameInfoProvider.currentFrameTimestamp = 0.016 + frameInfoProvider.nextFrameTimestamp = 0.024 + let thirdFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(thirdFps, 60) + + // fourth frame recorded + frameInfoProvider.currentFrameTimestamp = 0.032 + let fourthFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(fourthFps, 30) + } + + /* Rate representation + * + * 0----------8ms----------------------------33ms---------43ms + * | 8ms | 25ms | 10ms | + */ + func testFramesPerSecond_givenAdaptiveSyncDisplay() { + 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.008 + frameInfoProvider.nextFrameTimestamp = 0.033 + let secondFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(secondFps, 60) + + // third frame recorded + frameInfoProvider.currentFrameTimestamp = 0.033 + frameInfoProvider.nextFrameTimestamp = 0.043 + let thirdFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(thirdFps, 60) + + // fourth frame recorded + frameInfoProvider.currentFrameTimestamp = 0.043 + let fourthFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(fourthFps, 60) + } + + /* Rate representation + * + * 0----------8ms----------------------------33ms---------43ms + * | 8ms | 25ms | 10ms | + * skipped + */ + func testFramesPerSecond_givenAdaptiveSyncDisplayWithFreezingFrames() { + 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.008 + frameInfoProvider.nextFrameTimestamp = 0.033 + let secondFps = reader.framesPerSecond(provider: frameInfoProvider) + XCTAssertEqual(secondFps, 60) + + // third frame recorded + frameInfoProvider.currentFrameTimestamp = 0.043 + 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 { + var maximumDeviceFramesPerSecond: Int = 60 + var currentFrameTimestamp: CFTimeInterval = 0 + var nextFrameTimestamp: CFTimeInterval = 0 +} diff --git a/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift new file mode 100644 index 0000000000..8713dbcd0d --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/TelemetryReceiverTests.swift @@ -0,0 +1,50 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal + +@testable import DatadogRUM + +class TelemetryReceiverTests: XCTestCase { + // MARK: - Thread safety + + func testSendTelemetryAndReset_onAnyThread() { + let core = DatadogCoreProxy( + context: .mockWith( + version: .mockRandom(), + source: .mockAnySource(), + sdkVersion: .mockRandom() + ) + ) + defer { core.flushAndTearDown() } + + RUM.enable(with: .mockAny(), in: core) + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { core.telemetry.debug(id: .mockRandom(), message: "telemetry debug") }, + { core.telemetry.error(id: .mockRandom(), message: "telemetry error", kind: "error.kind", stack: "error.stack") }, + { core.telemetry.configuration(batchSize: .mockRandom()) }, + { + core.set( + baggage: [ + RUMContextAttributes.IDs.applicationID: String.mockRandom(), + RUMContextAttributes.IDs.sessionID: String.mockRandom(), + RUMContextAttributes.IDs.viewID: String.mockRandom(), + RUMContextAttributes.IDs.userActionID: String.mockRandom() + ], + forKey: "rum" + ) + } + ], + iterations: 50 + ) + // swiftlint:enable opening_brace + } +} diff --git a/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift b/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.swift new file mode 100644 index 0000000000..3f411d50d0 --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/UIApplicationSwizzlerTests.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 +@testable import DatadogRUM + +// TODO: RUMM-2034 Remove this flag once we have a host application for tests +#if !os(tvOS) + +class UIApplicationSwizzlerTests: XCTestCase { + private let handler = RUMActionsHandlerMock() + private lazy var swizzler = try! UIApplicationSwizzler(handler: handler) + + override func setUp() { + super.setUp() + swizzler.swizzle() + } + + override func tearDown() { + swizzler.unswizzle() + super.tearDown() + } + + func testWhenSendEventIsCalled_itNotifiesTheHandler() { + let expectation = self.expectation(description: "Notify handler") + + let anyApplication = UIApplication.shared + let anyEvent = UIEvent() + + handler.onSendEvent = { application, event in + XCTAssertTrue(application === anyApplication) + XCTAssertTrue(event === anyEvent) + expectation.fulfill() + } + + anyApplication.sendEvent(anyEvent) + + waitForExpectations(timeout: 1.5, handler: nil) + } +} + +#endif diff --git a/DatadogCore/Tests/Datadog/RUM/UIKitRUMViewsPredicateTests.swift b/DatadogCore/Tests/Datadog/RUM/UIKitRUMViewsPredicateTests.swift new file mode 100644 index 0000000000..28bccd95b0 --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/UIKitRUMViewsPredicateTests.swift @@ -0,0 +1,71 @@ +/* + * 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 DatadogRUM + +#if canImport(SwiftUI) +import SwiftUI +#endif + +class UIKitRUMViewsPredicateTests: XCTestCase { + func testGivenDefaultPredicate_whenAskingForCustomSwiftViewController_itNamesTheViewByItsClassName() { + // Given + let predicate = DefaultUIKitRUMViewsPredicate() + + // When + let customViewController = createMockView(viewControllerClassName: "CustomSwiftViewController") + let rumView = predicate.rumView(for: customViewController) + + // Then + XCTAssertEqual(rumView?.name, "CustomSwiftViewController") + XCTAssertEqual(rumView?.path, "CustomSwiftViewController") + XCTAssertTrue(rumView!.attributes.isEmpty) + } + + func testGivenDefaultPredicate_whenAskingForCustomObjcViewController_itNamesTheViewByItsClassName() { + // Given + let predicate = DefaultUIKitRUMViewsPredicate() + + // When + let customViewController = CustomObjcViewController() + let rumView = predicate.rumView(for: customViewController) + + // Then + XCTAssertEqual(rumView?.name, "CustomObjcViewController") + XCTAssertEqual(rumView?.path, "CustomObjcViewController") + XCTAssertTrue(rumView!.attributes.isEmpty) + } + + func testGivenDefaultPredicate_whenAskingUIKitViewController_itReturnsNoView() { + // Given + let predicate = DefaultUIKitRUMViewsPredicate() + + // When + let uiKitViewController = UIViewController() + let rumView = predicate.rumView(for: uiKitViewController) + + // Then + XCTAssertNil(rumView) + } + +#if canImport(SwiftUI) + func testGivenDefaultPredicate_whenAskingSwiftUIViewController_itReturnsNoView() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + // Given + let predicate = DefaultUIKitRUMViewsPredicate() + + // When + let swiftUIHostingController = UIHostingController(rootView: EmptyView()) + let rumView = predicate.rumView(for: swiftUIHostingController) + + // Then + XCTAssertNil(rumView) + } +#endif +} diff --git a/DatadogCore/Tests/Datadog/RUM/UIViewControllerSwizzlerTests.swift b/DatadogCore/Tests/Datadog/RUM/UIViewControllerSwizzlerTests.swift new file mode 100644 index 0000000000..c4ae6a37f2 --- /dev/null +++ b/DatadogCore/Tests/Datadog/RUM/UIViewControllerSwizzlerTests.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 XCTest +@testable import DatadogRUM + +private class ViewControllerMock: UIViewController { + var viewDidAppearExpectation: XCTestExpectation? + var viewDidDisappearExpectation: XCTestExpectation? + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + viewDidAppearExpectation?.fulfill() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + viewDidDisappearExpectation?.fulfill() + } +} + +class UIViewControllerSwizzlerTests: XCTestCase { + private let handler = UIKitRUMViewsHandlerMock() + private lazy var swizzler = try! UIViewControllerSwizzler(handler: handler) + + override func setUp() { + super.setUp() + swizzler.swizzle() + } + + override func tearDown() { + swizzler.unswizzle() + super.tearDown() + } + + func testWhenViewDidAppearIsCalled_itNotifiesTheHandlerBeforeTheUserMethodExecutes() { + let callOriginalMethodExpectation = expectation(description: "Call original method") + let notifyHandlerExpectation = expectation(description: "Notify handler") + + let viewController = ViewControllerMock() + viewController.viewDidAppearExpectation = callOriginalMethodExpectation + let animated = Bool.random() + + handler.notifyViewDidAppear = { receivedViewController, receivedAnimated in + XCTAssertTrue(receivedViewController === viewController) + XCTAssertEqual(receivedAnimated, animated) + notifyHandlerExpectation.fulfill() + } + + // When + viewController.viewDidAppear(animated) + + // Then + wait(for: [notifyHandlerExpectation, callOriginalMethodExpectation], timeout: 0.5, enforceOrder: true) + } + + func testWhenViewWillDisappearIsCalled_itNotifiesTheHandlerBeforeTheMethodExecutes() { + let callOriginalMethodExpectation = expectation(description: "Call original method") + let notifyHandlerExpectation = expectation(description: "Notify handler") + + let viewController = ViewControllerMock() + viewController.viewDidDisappearExpectation = callOriginalMethodExpectation + let animated = Bool.random() + + handler.notifyViewDidDisappear = { receivedViewController, receivedAnimated in + XCTAssertTrue(receivedViewController === viewController) + XCTAssertEqual(receivedAnimated, animated) + notifyHandlerExpectation.fulfill() + } + + // When + viewController.viewDidDisappear(animated) + + // Then + wait(for: [notifyHandlerExpectation, callOriginalMethodExpectation], timeout: 0.5, enforceOrder: true) + } +} diff --git a/DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift b/DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift new file mode 100644 index 0000000000..d02ee85b75 --- /dev/null +++ b/DatadogCore/Tests/Datadog/SDKMetrics/BatchMetricsTests.swift @@ -0,0 +1,30 @@ +/* + * 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 DatadogCore + +class BatchMetricsTests: XCTestCase { + func testBatchRemovalReasonFormatting() { + typealias RemovalReason = BatchDeletedMetric.RemovalReason + + XCTAssertEqual(RemovalReason.intakeCode(responseCode: 202).toString(), "intake-code-202") + XCTAssertEqual(RemovalReason.obsolete.toString(), "obsolete") + XCTAssertEqual(RemovalReason.purged.toString(), "purged") + XCTAssertEqual(RemovalReason.invalid.toString(), "invalid") + XCTAssertEqual(RemovalReason.flushed.toString(), "flushed") + } + + func testOnlyCertainBatchRemovalReasonsAreIncludedInMetric() { + typealias RemovalReason = BatchDeletedMetric.RemovalReason + + XCTAssertTrue(RemovalReason.intakeCode(responseCode: 202).includeInMetric) + XCTAssertTrue(RemovalReason.obsolete.includeInMetric) + XCTAssertTrue(RemovalReason.purged.includeInMetric) + XCTAssertTrue(RemovalReason.invalid.includeInMetric) + XCTAssertFalse(RemovalReason.flushed.includeInMetric) + } +} diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift new file mode 100644 index 0000000000..94ac387268 --- /dev/null +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -0,0 +1,961 @@ +/* + * 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 + +@testable import DatadogTrace +@testable import DatadogLogs +@testable import DatadogCore +@testable import DatadogRUM + +// swiftlint:disable multiline_arguments_brackets +class TracerTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var config: Trace.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + config = Trace.Configuration() + } + + override func tearDown() { + core.flushAndTearDown() + core = nil + config = nil + super.tearDown() + } + + // MARK: - Customizing Tracer + + func testSendingSpanWithDefaultTracer() throws { + core.context = .mockWith( + service: "default-service-name", + env: "custom", + version: "1.0.0", + source: "abc", + sdkVersion: "1.2.3", + ciAppOrigin: nil, + 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)) + config.spanIDGenerator = RelativeSpanIDGenerator(startingFrom: 100) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: "operation") + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + try spanMatcher.assertItFullyMatches(jsonString: """ + { + "spans": [ + { + "_dd.agent_psr": 1, + "trace_id": "64", + "span_id": "64", + "parent_id": "0", + "name": "operation", + "service": "default-service-name", + "resource": "operation", + "start": 1576404000000000000, + "duration": 500000000, + "error": 0, + "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, + "meta._dd.p.tid": "a" + } + ], + "env": "custom" + } + """) + } + + func testSendingSpanWithCustomizedTracer() throws { + config.service = "custom-service-name" + config.networkInfoEnabled = true + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: .mockAny()) + span.finish() + + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + XCTAssertEqual(try spanMatcher.serviceName(), "custom-service-name") + XCTAssertNoThrow(try spanMatcher.meta.networkAvailableInterfaces()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionIsExpensive()) + XCTAssertNoThrow(try spanMatcher.meta.networkReachability()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierAllowsVoIP()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierName()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionSupportsIPv4()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionSupportsIPv6()) + if #available(iOS 13.0, *) { + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionIsConstrained()) + } + } + + func testSendingSpanWithGlobalTags() throws { + config.service = "custom-service-name" + config.tags = [ + "globaltag1": "globalValue1", + "globaltag2": "globalValue2" + ] + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: .mockAny()) + span.setTag(key: "globaltag2", value: "overwrittenValue" ) + span.finish() + + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + XCTAssertEqual(try spanMatcher.serviceName(), "custom-service-name") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.globaltag1"), "globalValue1") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.globaltag2"), "overwrittenValue") + } + + // MARK: - Sending Customized Spans + + func testSendingCustomizedSpan() throws { + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan( + operationName: "operation", + tags: [ + "tag1": "string value", + "error": true, + SpanTags.resource: "GET /foo.png" + ], + startTime: .mockDecember15th2019At10AMUTC() + ) + span.setTag(key: "tag2", value: 123) + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + XCTAssertEqual(try spanMatcher.operationName(), "operation") + XCTAssertEqual(try spanMatcher.resource(), "GET /foo.png") + XCTAssertEqual(try spanMatcher.startTime(), 1_576_404_000_000_000_000) + XCTAssertEqual(try spanMatcher.duration(), 500_000_000) + XCTAssertEqual(try spanMatcher.isError(), 1) + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag1"), "string value") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag2"), "123") + } + + func testSendingSpanWithParentAndBaggageItems() throws { + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let rootSpan = tracer.startSpan(operationName: "root operation") + let childSpan = tracer.startSpan(operationName: "child operation", childOf: rootSpan.context) + let grandchildSpan = tracer.startSpan(operationName: "grandchild operation", childOf: childSpan.context) + rootSpan.setBaggageItem(key: "root-item", value: "foo") + childSpan.setBaggageItem(key: "child-item", value: "bar") + grandchildSpan.setBaggageItem(key: "grandchild-item", value: "bizz") + + grandchildSpan.setTag(key: "overwritten", value: "b") // This value "b" coming from a tag... + grandchildSpan.setBaggageItem(key: "overwritten", value: "a") // ... should overwrite this "a" coming from the baggage item. + + grandchildSpan.finish() + childSpan.finish() + rootSpan.finish() + + let spanMatchers = try core.waitAndReturnSpanMatchers() + let rootMatcher = spanMatchers[2] + let childMatcher = spanMatchers[1] + let grandchildMatcher = spanMatchers[0] + + // Assert child-parent relationship + + XCTAssertEqual(try grandchildMatcher.operationName(), "grandchild operation") + XCTAssertEqual(try grandchildMatcher.traceID(), rootSpan.context.dd.traceID) + XCTAssertEqual(try grandchildMatcher.parentSpanID(), childSpan.context.dd.spanID) + XCTAssertNil(try? grandchildMatcher.metrics.isRootSpan()) + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.child-item"), "bar") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.grandchild-item"), "bizz") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.grandchild-item"), "bizz") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.overwritten"), "b", "Tags should have higher priority than baggage items") + + XCTAssertEqual(try childMatcher.operationName(), "child operation") + XCTAssertEqual(try childMatcher.traceID(), rootSpan.context.dd.traceID) + XCTAssertEqual(try childMatcher.parentSpanID(), rootSpan.context.dd.spanID) + XCTAssertNil(try? childMatcher.metrics.isRootSpan()) + XCTAssertEqual(try childMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertEqual(try childMatcher.meta.custom(keyPath: "meta.child-item"), "bar") + XCTAssertNil(try? childMatcher.meta.custom(keyPath: "meta.grandchild-item")) + + XCTAssertEqual(try rootMatcher.operationName(), "root operation") + XCTAssertEqual(try rootMatcher.parentSpanID(), .invalid) + XCTAssertEqual(try rootMatcher.metrics.isRootSpan(), 1) + XCTAssertEqual(try rootMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertNil(try? rootMatcher.meta.custom(keyPath: "meta.child-item")) + XCTAssertNil(try? rootMatcher.meta.custom(keyPath: "meta.grandchild-item")) + + // Assert timing constraints + + XCTAssertGreaterThan(try grandchildMatcher.startTime(), try childMatcher.startTime()) + XCTAssertGreaterThan(try childMatcher.startTime(), try rootMatcher.startTime()) + XCTAssertLessThan(try grandchildMatcher.duration(), try childMatcher.duration()) + XCTAssertLessThan(try childMatcher.duration(), try rootMatcher.duration()) + } + + func testSendingSpanWithActiveSpanAsAParent() throws { + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let queue1 = DispatchQueue(label: "\(#function)-queue1") + let queue2 = DispatchQueue(label: "\(#function)-queue2") + + let rootSpan = tracer.startSpan(operationName: "root operation").setActive() + + queue1.sync { + let child1Span = tracer.startSpan(operationName: "child 1 operation") + child1Span.finish() + } + + queue2.sync { + let child2Span = tracer.startSpan(operationName: "child 2 operation") + child2Span.finish() + } + + rootSpan.finish() + + let spanMatchers = try core.waitAndReturnSpanMatchers() + let rootMatcher = spanMatchers[2] + let child1Matcher = spanMatchers[1] + let child2Matcher = spanMatchers[0] + + XCTAssertEqual(try rootMatcher.parentSpanID(), .invalid) + XCTAssertEqual(try child1Matcher.parentSpanID(), try rootMatcher.spanID()) + XCTAssertEqual(try child2Matcher.parentSpanID(), try rootMatcher.spanID()) + } + + func testSendingSpansWithNoParent() throws { + let expectation = self.expectation(description: "Complete 2 fake API requests") + expectation.expectedFulfillmentCount = 2 + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let queue = DispatchQueue(label: "\(#function)-queue") + + func makeAPIRequest(completion: @escaping () -> Void) { + queue.asyncAfter(deadline: .now() + 0.1) { + completion() + expectation.fulfill() + } + } + + let request1Span = tracer.startSpan(operationName: "/resource/1") + makeAPIRequest { + request1Span.finish() + } + + let request2Span = tracer.startSpan(operationName: "/resource/2") + makeAPIRequest { + request2Span.finish() + } + tracer.activeSpan?.finish() + + waitForExpectations(timeout: 5) + let spanMatchers = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(try spanMatchers[0].parentSpanID(), .invalid) + XCTAssertEqual(try spanMatchers[1].parentSpanID(), .invalid) + } + + func testStartingRootActiveSpanInAsynchronousJobs() throws { + let expectation = self.expectation(description: "Complete 2 fake API requests") + expectation.expectedFulfillmentCount = 2 + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let queue = DispatchQueue(label: "\(#function)") + + func makeFakeAPIRequest(on queue: DispatchQueue, completion: @escaping () -> Void) { + let requestSpan = tracer.startRootSpan(operationName: "request").setActive() + queue.asyncAfter(deadline: .now() + 0.1) { + let responseDecodingSpan = tracer.startSpan(operationName: "response decoding") + responseDecodingSpan.finish() + requestSpan.finish() + completion() + expectation.fulfill() + } + } + makeFakeAPIRequest(on: queue) {} + makeFakeAPIRequest(on: queue) {} + + waitForExpectations(timeout: 5) + let spanMatchers = try core.waitAndReturnSpanMatchers() + let response1Matcher = spanMatchers[0] + let request1Matcher = spanMatchers[1] + let response2Matcher = spanMatchers[2] + let request2Matcher = spanMatchers[3] + + XCTAssertEqual(try response1Matcher.parentSpanID(), try request1Matcher.spanID()) + XCTAssertEqual(try request1Matcher.parentSpanID(), .invalid) + XCTAssertEqual(try response2Matcher.parentSpanID(), try request2Matcher.spanID()) + XCTAssertEqual(try request2Matcher.parentSpanID(), .invalid) + } + + // MARK: - Sending user info + + func testSendingUserInfo() throws { + core.context = .mockWith( + userInfo: .empty + ) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core).dd + + tracer.startSpan(operationName: "span with no user info").finish() + + core.context.userInfo = UserInfo(id: "abc-123", name: "Foo", email: nil, extraInfo: [:]) + tracer.startSpan(operationName: "span with user `id` and `name`").finish() + + core.context.userInfo = UserInfo( + id: "abc-123", + name: "Foo", + email: "foo@example.com", + extraInfo: [ + "str": "value", + "int": 11_235, + "bool": true + ] + ) + tracer.startSpan(operationName: "span with user `id`, `name`, `email` and `extraInfo`").finish() + + core.context.userInfo = .empty + tracer.startSpan(operationName: "span with no user info").finish() + + let spanMatchers = try core.waitAndReturnSpanMatchers() + XCTAssertNil(try? spanMatchers[0].meta.userID()) + XCTAssertNil(try? spanMatchers[0].meta.userName()) + XCTAssertNil(try? spanMatchers[0].meta.userEmail()) + + XCTAssertEqual(try spanMatchers[1].meta.userID(), "abc-123") + XCTAssertEqual(try spanMatchers[1].meta.userName(), "Foo") + XCTAssertNil(try? spanMatchers[1].meta.userEmail()) + + XCTAssertEqual(try spanMatchers[2].meta.userID(), "abc-123") + XCTAssertEqual(try spanMatchers[2].meta.userName(), "Foo") + XCTAssertEqual(try spanMatchers[2].meta.userEmail(), "foo@example.com") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.str"), "value") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.int"), "11235") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.usr.bool"), "true") + + XCTAssertNil(try? spanMatchers[3].meta.userID()) + XCTAssertNil(try? spanMatchers[3].meta.userName()) + XCTAssertNil(try? spanMatchers[3].meta.userEmail()) + } + + // MARK: - Sending carrier info + + func testSendingCarrierInfoWhenEnteringAndLeavingCellularServiceRange() throws { + core.context = .mockWith( + carrierInfo: nil + ) + + config.networkInfoEnabled = true + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core).dd + + // simulate entering cellular service range + core.context.carrierInfo = .mockWith( + carrierName: "Carrier", + carrierISOCountryCode: "US", + carrierAllowsVOIP: true, + radioAccessTechnology: .LTE + ) + + tracer.startSpan(operationName: "span with carrier info").finish() + + // simulate leaving cellular service range + core.context.carrierInfo = nil + + tracer.startSpan(operationName: "span with no carrier info").finish() + + let spanMatchers = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierName(), "Carrier") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierISOCountryCode(), "US") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierRadioTechnology(), "LTE") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierAllowsVoIP(), "1") + + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierName()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierAllowsVoIP()) + } + + // MARK: - Sending network info + + func testSendingNetworkConnectionInfoWhenReachabilityChanges() throws { + core.context = .mockWith(networkConnectionInfo: nil) + + config.networkInfoEnabled = true + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core).dd + + // simulate reachable network + core.context.networkConnectionInfo = .mockWith( + reachability: .yes, + availableInterfaces: [.wifi, .cellular], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: true, + isConstrained: true + ) + + tracer.startSpan(operationName: "online span").finish() + + // simulate unreachable network + core.context.networkConnectionInfo = .mockWith( + reachability: .no, + availableInterfaces: [], + supportsIPv4: false, + supportsIPv6: false, + isExpensive: false, + isConstrained: false + ) + + tracer.startSpan(operationName: "offline span").finish() + + let spanMatchers = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(try spanMatchers[0].meta.networkReachability(), "yes") + XCTAssertEqual(try spanMatchers[0].meta.networkAvailableInterfaces(), "wifi+cellular") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionIsConstrained(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionIsExpensive(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionSupportsIPv4(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionSupportsIPv6(), "1") + + XCTAssertEqual(try? spanMatchers[1].meta.networkReachability(), "no") + XCTAssertNil(try? spanMatchers[1].meta.networkAvailableInterfaces()) + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionIsConstrained(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionIsExpensive(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionSupportsIPv4(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionSupportsIPv6(), "0") + } + + // MARK: - Sending tags + + func testSendingSpanTagsOfDifferentEncodableValues() throws { + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + tracer.dd.spanEventBuilder.attributesEncoder.outputFormatting = [.sortedKeys] + + let span = tracer.startSpan(operationName: "operation", tags: [:], startTime: .mockDecember15th2019At10AMUTC()) + + // string literal + span.setTag(key: "string", value: "hello") + + // boolean literal + span.setTag(key: "bool", value: true) + + // integer literal + span.setTag(key: "int", value: 10) + + // Typed 8-bit unsigned Integer + span.setTag(key: "uint-8", value: UInt8(10)) + + // double-precision, floating-point value + span.setTag(key: "double", value: 10.5) + + // array of `Encodable` integer + span.setTag(key: "array-of-int", value: [1, 2, 3]) + + // dictionary of `Encodable` date types + span.setTag(key: "dictionary-with-date", value: [ + "date": Date.mockDecember15th2019At10AMUTC(), + ]) + + struct Person: Codable { + let name: String + let age: Int + let nationality: String + } + + // custom `Encodable` structure + span.setTag(key: "person", value: Person(name: "Adam", age: 30, nationality: "Polish")) + + // nested string literal + span.setTag(key: "nested.string", value: "hello") + + // URL + span.setTag(key: "url", value: URL(string: "https://example.com/image.png")!) + + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + XCTAssertEqual(try spanMatcher.operationName(), "operation") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.string"), "hello") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.bool"), "true") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.int"), "10") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.uint-8"), "10") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.double"), "10.5") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.array-of-int"), "[1,2,3]") + XCTAssertEqual( + try spanMatcher.meta.custom(keyPath: "meta.dictionary-with-date"), + #"{"date":"2019-12-15T10:00:00.000Z"}"# + ) + XCTAssertEqual( + try spanMatcher.meta.custom(keyPath: "meta.person"), + #"{"age":30,"name":"Adam","nationality":"Polish"}"# + ) + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.nested.string"), "hello") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.url"), "https://example.com/image.png") + } + + // MARK: - Integration With Logging Feature + + func testSendingSpanLogs() throws { + let logging: LogsFeature = .mockWith( + messageReceiver: LogMessageReceiver.mockAny() + ) + try core.register(feature: logging) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: "operation", startTime: .mockDecember15th2019At10AMUTC()) + span.log(fields: [OTLogFields.message: "hello", "custom.field": "value"]) + span.log(fields: [OTLogFields.event: "error", OTLogFields.errorKind: "Swift error", OTLogFields.message: "Ops!"]) + + let logMatchers = try core.waitAndReturnLogMatchers() + + let regularLogMatcher = logMatchers[0] + let errorLogMatcher = logMatchers[1] + + regularLogMatcher.assertStatus(equals: "info") + regularLogMatcher.assertMessage(equals: "hello") + regularLogMatcher.assertValue(forKey: "dd.trace_id", equals: String(span.context.dd.traceID, representation: .hexadecimal)) + regularLogMatcher.assertValue(forKey: "dd.span_id", equals: String(span.context.dd.spanID, representation: .hexadecimal)) + regularLogMatcher.assertValue(forKey: "custom.field", equals: "value") + + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Swift error") + errorLogMatcher.assertValue(forKey: "error.message", equals: "Ops!") + errorLogMatcher.assertMessage(equals: "Ops!") + errorLogMatcher.assertValue(forKey: "dd.trace_id", equals: String(span.context.dd.traceID, representation: .hexadecimal)) + errorLogMatcher.assertValue(forKey: "dd.span_id", equals: String(span.context.dd.spanID, representation: .hexadecimal)) + } + + func testSendingSpanLogsWithErrorFromArguments() throws { + let logging: LogsFeature = .mockWith( + messageReceiver: LogMessageReceiver.mockAny() + ) + try core.register(feature: logging) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: "operation", startTime: .mockDecember15th2019At10AMUTC()) + span.log(fields: [OTLogFields.message: "hello", "custom.field": "value"]) + span.setError(kind: "Swift error", message: "Ops!") + + let logMatchers = try core.waitAndReturnLogMatchers() + let errorLogMatcher = logMatchers[1] + + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Swift error") + errorLogMatcher.assertValue(forKey: "error.message", equals: "Ops!") + errorLogMatcher.assertMessage(equals: "Ops!") + errorLogMatcher.assertValue(forKey: "dd.trace_id", equals: String(span.context.dd.traceID, representation: .hexadecimal)) + errorLogMatcher.assertValue(forKey: "dd.span_id", equals: String(span.context.dd.spanID, representation: .hexadecimal)) + } + + func testSendingSpanLogsWithErrorFromNSError() throws { + let logging: LogsFeature = .mockWith( + messageReceiver: LogMessageReceiver.mockAny() + ) + try core.register(feature: logging) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: "operation", startTime: .mockDecember15th2019At10AMUTC()) + span.log(fields: [OTLogFields.message: "hello", "custom.field": "value"]) + let error = NSError( + domain: "Tracer", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Ops!"] + ) + span.setError(error) + + let logMatchers = try core.waitAndReturnLogMatchers() + + let errorLogMatcher = logMatchers[1] + + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Tracer - 1") + errorLogMatcher.assertValue(forKey: "error.message", equals: "Ops!") + errorLogMatcher.assertMessage(equals: "Ops!") + errorLogMatcher.assertValue(forKey: "dd.trace_id", equals: String(span.context.dd.traceID, representation: .hexadecimal)) + errorLogMatcher.assertValue(forKey: "dd.span_id", equals: String(span.context.dd.spanID, representation: .hexadecimal)) + } + + func testSendingSpanLogsWithErrorFromSwiftError() throws { + let logging: LogsFeature = .mockWith( + messageReceiver: LogMessageReceiver.mockAny() + ) + try core.register(feature: logging) + + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: "operation", startTime: .mockDecember15th2019At10AMUTC()) + span.log(fields: [OTLogFields.message: "hello", "custom.field": "value"]) + span.setError(ErrorMock("Ops!")) + + let logMatchers = try core.waitAndReturnLogMatchers() + + let errorLogMatcher = logMatchers[1] + + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "ErrorMock") + errorLogMatcher.assertValue(forKey: "error.message", equals: "Ops!") + errorLogMatcher.assertMessage(equals: "Ops!") + errorLogMatcher.assertValue(forKey: "dd.trace_id", equals: String(span.context.dd.traceID, representation: .hexadecimal)) + errorLogMatcher.assertValue(forKey: "dd.span_id", equals: String(span.context.dd.spanID, representation: .hexadecimal)) + } + + // MARK: - Integration With RUM Feature + + func testGivenBundleWithRumEnabled_whenSendingSpanBeforeAnyInteraction_itContainsViewId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], "rum-app-id") + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumViewID], rumEvent.view.id) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } + + func testGivenBundleWithRumEnabled_whenStartingSpanWhileUserInteractionIsPending_itContainsActionId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + + // When + RUMMonitor.shared(in: core).startAction(type: .swipe, name: "swipe") + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + RUMMonitor.shared(in: core).stopAction(type: .swipe, name: "swipe") + span.finish() + + // Then + let rumEvent = try XCTUnwrap( + core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMActionEvent.self).first(where: { $0.action.type == .swipe }) + ) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], rumEvent.application.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumViewID], rumEvent.view.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumActionID], rumEvent.action.id) + } + + func testGivenBundleWithRumEnabled_whenSendingSpanAfterViewIsStopped_itContainsSessionId() throws { + config.bundleWithRumEnabled = true + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + RUMMonitor.shared(in: core).startView(key: "view", name: "view") + + // When + RUMMonitor.shared(in: core).stopView(key: "view") + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last) + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertEqual(spanEvent.tags[SpanTags.rumApplicationID], rumEvent.application.id) + XCTAssertEqual(spanEvent.tags[SpanTags.rumSessionID], rumEvent.session.id) + XCTAssertNil(spanEvent.tags[SpanTags.rumViewID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } + + func testGivenBundleWithRumDisabled_whenSendingSpan_itDoesNotContainRUMContext() throws { + config.bundleWithRumEnabled = false + Trace.enable(with: config, in: core) + + // Given + RUM.enable(with: .init(applicationID: "rum-app-id"), in: core) + RUMMonitor.shared(in: core).startView(key: "view", name: "view") + + // When + let span = Tracer.shared(in: core).startSpan(operationName: "operation") + span.finish() + + // Then + let spanEvent = try XCTUnwrap(core.waitAndReturnSpanEvents().first) + XCTAssertNil(spanEvent.tags[SpanTags.rumApplicationID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumSessionID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumViewID]) + XCTAssertNil(spanEvent.tags[SpanTags.rumActionID]) + } + + // MARK: - Injecting span context into carrier + + func testInjectingAndExtractingSpanContextUsingDatadogCarrier() { + // Given + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context + + // When + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + tracer.inject(spanContext: injectedContext, writer: writer) + + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! + + // Then + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) + } + + func testInjectingAndExtractingSpanContextUsingB3SingleCarrier() { + // Given + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context + + // When + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .single, traceContextInjection: .all) + tracer.inject(spanContext: injectedContext, writer: writer) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! + + // Then + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) + } + + func testInjectingAndExtractingSpanContextUsingB3MultipleCarrier() { + // Given + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context + + // When + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .multiple, traceContextInjection: .all) + tracer.inject(spanContext: injectedContext, writer: writer) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! + + // Then + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) + } + + func testInjectingAndExtractingSpanContextUsingW3CCarrier() { + // Given + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + let injectedContext = tracer.startSpan(operationName: .mockAny()).context + + // When + let writer = W3CHTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + tracer.inject(spanContext: injectedContext, writer: writer) + + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + let extractedContext = tracer.extract(reader: reader)! + + // Then + XCTAssertEqual(injectedContext.dd.traceID, extractedContext.dd.traceID) + XCTAssertEqual(injectedContext.dd.spanID, extractedContext.dd.spanID) + XCTAssertEqual(injectedContext.dd.parentSpanID, extractedContext.dd.parentSpanID) + XCTAssertEqual(injectedContext.dd.sampleRate, extractedContext.dd.sampleRate) + XCTAssertEqual(injectedContext.dd.isKept, extractedContext.dd.isKept) + } + + // MARK: - Span Dates Correction + + func testGivenTimeDifferenceBetweenDeviceAndServer_whenCollectingSpans_thenSpanDateUsesServerTime() throws { + // Given + let deviceTime: Date = .mockDecember15th2019At10AMUTC() + let serverTimeOffset = TimeInterval.random(in: -5..<5).rounded() // few seconds difference + + core.context = .mockWith( + serverTimeOffset: serverTimeOffset + ) + + // When + config.dateProvider = RelativeDateProvider(using: deviceTime) + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + let span = tracer.startSpan(operationName: .mockAny()) + span.finish(at: deviceTime.addingTimeInterval(2)) // 2 seconds long span + + // Then + let spanMatcher = try core.waitAndReturnSpanMatchers()[0] + XCTAssertEqual( + try spanMatcher.startTime(), + deviceTime.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toNanoseconds, + "The `startTime` should be using server time." + ) + XCTAssertEqual( + try spanMatcher.duration(), + 2_000_000_000, + "The `duration` should remain unaffected." + ) + } + + // MARK: - Thread safety + + func testRandomlyCallingDifferentAPIsConcurrentlyDoesNotCrash() { + Trace.enable(with: config, in: core) + let tracer = Tracer.shared(in: core) + + var spans: [DDSpan] = [] + let queue = DispatchQueue(label: "spans-array-sync") + + // Start 20 spans concurrently + DispatchQueue.concurrentPerform(iterations: 20) { iteration in + let span = tracer.startSpan(operationName: "operation \(iteration)", childOf: nil).dd + queue.async { spans.append(span) } + } + + queue.sync {} // wait for all spans in the array + + /// Calls given closures on each span concurrently + func testThreadSafety(closures: [(DDSpan) -> Void]) { + DispatchQueue.concurrentPerform(iterations: 100) { iteration in + closures.forEach { closure in + closure(spans[iteration % spans.count]) + } + } + } + + testThreadSafety( + closures: [ + // swiftlint:disable opening_brace + { span in span.setTag(key: .mockRandom(among: .alphanumerics, length: 1), value: "value") }, + { span in span.setBaggageItem(key: .mockRandom(among: .alphanumerics, length: 1), value: "value") }, + { span in _ = span.baggageItem(withKey: .mockRandom(among: .alphanumerics)) }, + { span in _ = span.context.forEachBaggageItem { _, _ in return false } }, + { span in span.log(fields: [.mockRandom(among: .alphanumerics, length: 1): "value"]) }, + { span in span.finish() } + // swiftlint:enable opening_brace + ] + ) + } + + // MARK: - Usage errors + + func testGivenSDKNotInitialized_whenObtainingSharedTracer_itPrintsError() { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { message, _ in print(message) } } + + // given + let core = NOPDatadogCore() + Trace.enable(in: core) + + // when + let tracer = Tracer.shared(in: core) + + // then + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: Datadog SDK must be initialized and RUM feature must be enabled before calling `Tracer.shared(in:)`." + ) + XCTAssertTrue(tracer is DDNoopTracer) + } + + func testGivenTraceNotEnabled_whenObtainingSharedTracer_itPrintsError() { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { message, _ in print(message) } } + + // given + let core = FeatureRegistrationCoreMock() + XCTAssertNil(core.get(feature: TraceFeature.self)) + + // when + let tracer = Tracer.shared(in: core) + + // then + XCTAssertEqual( + printFunction.printedMessage, + "🔥 Datadog SDK usage error: Trace feature must be enabled before calling `Tracer.shared(in:)`." + ) + XCTAssertTrue(tracer is DDNoopTracer) + } + + func testGivenLoggingFeatureNotEnabled_whenSendingLogFromSpan_itPrintsWarning() throws { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + // given + XCTAssertNil(core.get(feature: LogsFeature.self)) + Trace.enable(in: core) + + // when + let tracer = Tracer.shared(in: core) + let span = tracer.startSpan(operationName: "foo") + span.log(fields: ["bar": "bizz"]) + + // then + core.flush() + XCTAssertEqual(dd.logger.warnLog?.message, "The log for span \"foo\" will not be send, because the Logs feature is not enabled.") + } +} +// swiftlint:enable multiline_arguments_brackets diff --git a/DatadogCore/Tests/Datadog/Tracing/DDSpanTests.swift b/DatadogCore/Tests/Datadog/Tracing/DDSpanTests.swift new file mode 100644 index 0000000000..9dd482c092 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Tracing/DDSpanTests.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 XCTest +import TestUtilities +import DatadogInternal + +@testable import DatadogLogs +@testable import DatadogTrace + +class DDSpanTests: XCTestCase { + // MARK: - Sending Span Logs + + func testWhenLoggingSpanEvent_itWritesLogToLogOutput() throws { + let core = DatadogCoreProxy() + defer { core.flushAndTearDown() } + + Logs.enable(in: core) + Trace.enable(in: core) + + // Given + let tracer = Tracer.shared(in: core) + let span = tracer.startSpan(operationName: .mockAny()) + + // When + let log1Fields = mockRandomAttributes() + span.log(fields: log1Fields) + + let log2Fields = mockRandomAttributes() + span.log(fields: log2Fields) + + // Then + let logs: [LogEvent] = core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self) + XCTAssertEqual(logs.count, 2, "It should send 2 logs") + DDAssertJSONEqual( + AnyEncodable(logs[0].attributes.userAttributes), + AnyEncodable(log1Fields) + ) + DDAssertJSONEqual( + AnyEncodable(logs[1].attributes.userAttributes), + AnyEncodable(log2Fields) + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift new file mode 100644 index 0000000000..ca87c74455 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Tracing/DatadogTraceFeatureTests.swift @@ -0,0 +1,164 @@ +/* + * 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 + +@testable import DatadogTrace +@testable import DatadogCore + +class DatadogTraceFeatureTests: XCTestCase { + override func setUp() { + super.setUp() + temporaryCoreDirectory.create() + } + + override func tearDown() { + temporaryCoreDirectory.delete() + super.tearDown() + } + + // MARK: - HTTP Message + + func testItUsesExpectedHTTPMessage() throws { + let randomApplicationName: String = .mockRandom(among: .alphanumerics) + let randomApplicationVersion: String = .mockRandom() + let randomSource: String = .mockRandom() + let randomOrigin: String = .mockRandom() + let randomSDKVersion: String = .mockRandom(among: .alphanumerics) + let randomUploadURL: URL = .mockRandom() + let randomClientToken: String = .mockRandom() + let randomDeviceName: String = .mockRandom() + let randomDeviceOSName: String = .mockRandom() + let randomDeviceOSVersion: String = .mockRandom() + let randomEncryption: DataEncryption? = Bool.random() ? DataEncryptionMock() : nil + let randomBackgroundTasksEnabled: Bool = .mockRandom() + + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let httpClient = URLSessionClient(session: server.getInterceptedURLSession()) + + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .combining( + storagePerformance: .writeEachObjectToNewFileAndReadAllFiles, + uploadPerformance: .veryQuick + ), + httpClient: httpClient, + encryption: randomEncryption, + contextProvider: .mockWith( + context: .mockWith( + clientToken: randomClientToken, + version: randomApplicationVersion, + source: randomSource, + sdkVersion: randomSDKVersion, + ciAppOrigin: randomOrigin, + applicationName: randomApplicationName, + device: .mockWith( + name: randomDeviceName, + osName: randomDeviceOSName, + osVersion: randomDeviceOSVersion + ) + ) + ), + applicationVersion: randomApplicationVersion, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: randomBackgroundTasksEnabled + ) + defer { core.flushAndTearDown() } + + // Given + Trace.enable(with: .init(customEndpoint: randomUploadURL), in: core) + + // When + let tracer = Tracer.shared(in: core) + let span = tracer.startSpan(operationName: .mockAny()) + span.finish() + + // Then + let request = server.waitAndReturnRequests(count: 1)[0] + let requestURL = try XCTUnwrap(request.url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(requestURL.host, randomUploadURL.host) + XCTAssertEqual(requestURL.path, randomUploadURL.path) + XCTAssertNil(requestURL.query) + XCTAssertEqual( + request.allHTTPHeaderFields?["User-Agent"], + """ + \(randomApplicationName)/\(randomApplicationVersion) CFNetwork (\(randomDeviceName); \(randomDeviceOSName)/\(randomDeviceOSVersion)) + """ + ) + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "text/plain;charset=UTF-8") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Encoding"], "deflate") + XCTAssertEqual(request.allHTTPHeaderFields?["DD-API-KEY"], randomClientToken) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN"], randomOrigin) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-EVP-ORIGIN-VERSION"], randomSDKVersion) + XCTAssertEqual(request.allHTTPHeaderFields?["DD-REQUEST-ID"]?.matches(regex: .uuidRegex), true) + } + + // MARK: - HTTP Payload + + func testItUsesExpectedPayloadFormatForUploads() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let httpClient = URLSessionClient(session: server.getInterceptedURLSession()) + + let core = DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: .granted, + performance: .combining( + storagePerformance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write all events to single file, + minFileAgeForRead: StoragePerformanceMock.readAllFiles.minFileAgeForRead, + maxFileAgeForRead: StoragePerformanceMock.readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 3, // write 3 spans to payload, + maxObjectSize: .max + ), + uploadPerformance: UploadPerformanceMock( + initialUploadDelay: 0.5, // wait enough until events are written, + minUploadDelay: 1, + maxUploadDelay: 1, + uploadDelayChangeRate: 0 + ) + ), + httpClient: httpClient, + encryption: nil, + contextProvider: .mockAny(), + applicationVersion: .mockAny(), + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) + defer { core.flushAndTearDown() } + + // Given + Trace.enable(in: core) + + // When + let tracer = Tracer.shared(in: core) + + tracer.startSpan(operationName: "operation 1").finish() + tracer.startSpan(operationName: "operation 2").finish() + tracer.startSpan(operationName: "operation 3").finish() + + let payload = try XCTUnwrap(server.waitAndReturnRequests(count: 1)[0].httpBody) + + // Expected payload format: + // ``` + // span1JSON + // span2JSON + // span3JSON + // ``` + + let spanMatchers = try SpanMatcher.fromNewlineSeparatedJSONObjectsData(payload) + XCTAssertEqual(try spanMatchers[0].operationName(), "operation 1") + XCTAssertEqual(try spanMatchers[1].operationName(), "operation 2") + XCTAssertEqual(try spanMatchers[2].operationName(), "operation 3") + } +} diff --git a/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift b/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift new file mode 100644 index 0000000000..6590c9f589 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Tracing/OTelSpanTests.swift @@ -0,0 +1,115 @@ +/* + * 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 OpenTelemetryApi + +@testable import DatadogLogs +@testable import DatadogTrace + +final class OTelSpanTests: XCTestCase { + func testAddEvent() { + let core = DatadogCoreProxy() + defer { core.flushAndTearDown() } + + Logs.enable(in: core) + Trace.enable(in: core) + + // Given + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + let span = tracer + .spanBuilder(spanName: "OperationName") + .startSpan() + + // When + let attributes: [String: OpenTelemetryApi.AttributeValue] = .leafMock() + span.addEvent(name: "Otel Span Event", attributes: attributes, timestamp: Date()) + + // Then + let logs: [LogEvent] = core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self) + XCTAssertEqual(logs.count, 0) + } + + func testContextProviderSetActive_givenParentSpan() throws { + let core = DatadogCoreProxy() + defer { core.flushAndTearDown() } + + Trace.enable(in: core) + + // Given + OpenTelemetry.registerTracerProvider( + tracerProvider: OTelTracerProvider(in: core) + ) + + let tracer = OpenTelemetry + .instance + .tracerProvider + .get(instrumentationName: "", instrumentationVersion: nil) + + let parentSpan = tracer + .spanBuilder(spanName: "ParentSpan") + .startSpan() + + // When + OpenTelemetry.instance.contextProvider.setActiveSpan(parentSpan) + + let childSpan = tracer + .spanBuilder(spanName: "ChildSpan") + .startSpan() + + childSpan.end() + parentSpan.end() + + // Then + let spans = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(spans.count, 2) + + let childSpanMatcher = spans[0] + let parentSpanMatcher = spans[1] + + XCTAssertEqual(try parentSpanMatcher.traceID(), try childSpanMatcher.traceID()) + XCTAssertEqual(try parentSpanMatcher.spanID(), try childSpanMatcher.parentSpanID()) + } +} + +extension Dictionary where Key == String, Value == OpenTelemetryApi.AttributeValue { + static func mock() -> Self { + return [ + "string": .string("value"), + "bool": .bool(true), + "int": .int(2), + "double": .double(2.0), + "stringArray": .stringArray(["value1", "value2"]), + "boolArray": .boolArray([true, false]), + "intArray": .intArray([1, 2]), + "doubleArray": .doubleArray([1.0, 2.0]), + "set": .set(.init(labels: .leafMock())) + ] + } + + static func leafMock() -> Self { + return [ + "string": .string("value"), + "bool": .bool(true), + "int": .int(2), + "double": .double(2.0), + "stringArray": .stringArray(["value1", "value2"]), + "boolArray": .boolArray([true, false]), + "intArray": .intArray([1, 2]), + "doubleArray": .doubleArray([1.0, 2.0]) + ] + } +} diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift new file mode 100644 index 0000000000..66c2860978 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -0,0 +1,233 @@ +/* + * 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 DatadogLogs +@testable import DatadogTrace + +class TracingURLSessionHandlerTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + var core: PassthroughCoreMock! + var tracer: DatadogTracer! + var handler: TracingURLSessionHandler! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + let receiver = ContextMessageReceiver() + core = PassthroughCoreMock(messageReceiver: CombinedFeatureMessageReceiver([ + LogMessageReceiver.mockAny(), + receiver + ])) + + tracer = .mockWith( + core: core, + traceIDGenerator: RelativeTracingUUIDGenerator(startingFrom: .init(idHi: 10, idLo: 100)), + spanIDGenerator: RelativeSpanIDGenerator(startingFrom: 100, advancingByCount: 0), + loggingIntegration: TracingWithLoggingIntegration(core: core, service: .mockAny(), networkInfoEnabled: .mockAny()) + ) + + handler = TracingURLSessionHandler( + tracer: tracer, + contextReceiver: receiver, + distributedTraceSampler: .mockKeepAll(), + firstPartyHosts: .init([ + "www.example.com": [.datadog] + ]), + traceContextInjection: .all + ) + } + + override func tearDown() { + core = nil + tracer = nil + handler = nil + super.tearDown() + } + + func testGivenFirstPartyInterceptionWithNoError_itDoesNotSendLog() throws { + core.expectation = expectation(description: "Send span") + + // Given + let request: ImmutableRequest = .mockWith(httpMethod: "POST") + let interception = URLSessionTaskInterception(request: request, isFirstParty: true) + interception.register(metrics: .mockAny()) + interception.register(response: .mockResponseWith(statusCode: 200), error: nil) + + // When + handler.interceptionDidComplete(interception: interception) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let envelope: SpanEventsEnvelope? = core.events().last + XCTAssertNotNil(envelope?.spans.first) + + let log: LogEvent? = core.events().last + XCTAssertNil(log) + } + + func testGivenFirstPartyInterceptionWithNetworkError_whenInterceptionCompletes_itEncodesRequestInfoInSpanAndSendsLog() throws { + core.expectation = expectation(description: "Send span and log") + core.expectation?.expectedFulfillmentCount = 2 + + // Given + let request: ImmutableRequest = .mockWith( + url: URL(string: "http://www.example.com")!, + httpMethod: "GET" + ) + let error = NSError(domain: "domain", code: 123, userInfo: [NSLocalizedDescriptionKey: "network error"]) + let interception = URLSessionTaskInterception(request: request, isFirstParty: true) + interception.register(response: nil, error: error) + interception.register( + metrics: .mockWith( + fetch: .init( + start: .mockDecember15th2019At10AMUTC(), + end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 30) + ) + ) + ) + + // When + handler.interceptionDidComplete(interception: interception) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let envelope: SpanEventsEnvelope? = core.events().last + let span = try XCTUnwrap(envelope?.spans.first) + XCTAssertEqual(span.operationName, "urlsession.request") + XCTAssertEqual(span.resource, "http://www.example.com") + XCTAssertEqual(span.duration, 30) + XCTAssertTrue(span.isError) + 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, 8) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send error log") + XCTAssertEqual(log.status, .error) + XCTAssertEqual(log.message, "network error") + XCTAssertEqual( + log.attributes.internalAttributes?["dd.trace_id"] as? AnyCodable, + AnyCodable(String(span.traceID, representation: .hexadecimal)) + ) + XCTAssertEqual( + log.attributes.internalAttributes?["dd.trace_id"] as? AnyCodable, + AnyCodable(String(span.traceID, representation: .hexadecimal)) + ) + XCTAssertEqual( + log.attributes.internalAttributes?["dd.span_id"] as? AnyCodable, + AnyCodable(String(span.spanID, representation: .hexadecimal)) + ) + XCTAssertEqual(log.error?.kind, "domain - 123") + XCTAssertEqual(log.attributes.internalAttributes?.count, 2) + DDAssertJSONEqual( + AnyEncodable(log.attributes.userAttributes[OTLogFields.event]), + "error" + ) + XCTAssertEqual( + log.error?.stack, + "Error Domain=domain Code=123 \"network error\" UserInfo={NSLocalizedDescription=network error}" + ) + XCTAssertEqual(log.attributes.userAttributes.count, 1) + } + + func testGivenFirstPartyInterceptionWithClientError_whenInterceptionCompletes_itEncodesRequestInfoInSpanAndSendsLog() throws { + core.expectation = expectation(description: "Send span and log") + core.expectation?.expectedFulfillmentCount = 2 + + // Given + let request: ImmutableRequest = .mockWith(httpMethod: "GET") + let interception = URLSessionTaskInterception(request: request, isFirstParty: true) + interception.register(response: .mockResponseWith(statusCode: 404), error: nil) + interception.register( + metrics: .mockWith( + fetch: .init( + start: .mockDecember15th2019At10AMUTC(), + end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 2) + ) + ) + ) + + // When + handler.interceptionDidComplete(interception: interception) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let envelope: SpanEventsEnvelope? = core.events().last + let span = try XCTUnwrap(envelope?.spans.first) + XCTAssertEqual(span.operationName, "urlsession.request") + XCTAssertEqual(span.resource, "404") + XCTAssertEqual(span.duration, 2) + XCTAssertTrue(span.isError) + XCTAssertEqual(span.tags[OTTags.httpUrl], request.url!.absoluteString) + XCTAssertEqual(span.tags[OTTags.httpMethod], "GET") + 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, 9) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send error log") + XCTAssertEqual(log.status, .error) + XCTAssertEqual(log.message, "404 not found") + DDAssertJSONEqual( + AnyEncodable(log.attributes.internalAttributes?["dd.trace_id"]), + String(span.traceID, representation: .hexadecimal) + ) + DDAssertJSONEqual( + AnyEncodable(log.attributes.internalAttributes?["dd.span_id"]), + String(span.spanID, representation: .hexadecimal) + ) + XCTAssertEqual(log.error?.kind, "HTTPURLResponse - 404") + XCTAssertEqual(log.attributes.internalAttributes?.count, 2) + DDAssertJSONEqual( + AnyEncodable(log.attributes.userAttributes[OTLogFields.event]), + "error" + ) + XCTAssertEqual( + log.error?.stack, + "Error Domain=HTTPURLResponse Code=404 \"404 not found\" UserInfo={NSLocalizedDescription=404 not found}" + ) + XCTAssertEqual(log.attributes.userAttributes.count, 1) + } + + func testGivenAllTracingHeaderTypes_itUsesTheSameIds() throws { + let request: URLRequest = .mockWith(httpMethod: "GET") + let (modifiedRequest, _) = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) + + XCTAssertEqual( + modifiedRequest.allHTTPHeaderFields, + [ + "traceparent": "00-000000000000000a0000000000000064-0000000000000064-01", + "X-B3-SpanId": "0000000000000064", + "X-B3-Sampled": "1", + "X-B3-TraceId": "000000000000000a0000000000000064", + "b3": "000000000000000a0000000000000064-0000000000000064-1", + "x-datadog-trace-id": "100", + "x-datadog-tags": "_dd.p.tid=a", + "tracestate": "dd=p:0000000000000064;s:1", + "x-datadog-parent-id": "100", + "x-datadog-sampling-priority": "1" + ] + ) + } +} diff --git a/DatadogCore/Tests/Datadog/URLSessionAutoInstrumentation/Interception/URLFiltering/FirstPartyURLsFilterTests.swift b/DatadogCore/Tests/Datadog/URLSessionAutoInstrumentation/Interception/URLFiltering/FirstPartyURLsFilterTests.swift new file mode 100644 index 0000000000..e453a1da8f --- /dev/null +++ b/DatadogCore/Tests/Datadog/URLSessionAutoInstrumentation/Interception/URLFiltering/FirstPartyURLsFilterTests.swift @@ -0,0 +1,103 @@ +/* + * 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 DatadogCore + +class FirstPartyURLsFilterTests: XCTestCase { + let fixtures1stParty = [ + "http://first-party.com/", + "https://first-party.com/", + "https://api.first-party.com/v2/users", + "https://www.first-party.com/", + "https://login:p4ssw0rd@first-party.com:999/", + "http://any-domain.eu/", + "https://any-domain.eu/", + "https://api.any-domain.eu/v2/users", + "https://www.any-domain.eu/", + "https://login:p4ssw0rd@www.any-domain.eu:999/", + "https://api.any-domain.org.eu/", + ] + + let fixtures3rdParty = [ + "http://third-party.com/", + "https://third-party.com/", + "https://api.third-party.com/v2/users", + "https://www.third-party.com/", + "https://login:p4ssw0rd@third-party.com:999/", + "http://any-domain.org/", + "https://any-domain.org/", + "https://api.any-domain.org/v2/users", + "https://www.any-domain.org/", + "https://login:p4ssw0rd@www.any-domain.org:999/", + "https://api.any-domain.eu.org/", + ] + + func testWhenFilterIsInitializedWithEmptySet_itNeverReturnsFirstParty() { + let filter = FirstPartyURLsFilter(hosts: [:]) + (fixtures1stParty + fixtures3rdParty).forEach { fixture in + let url = URL(string: fixture)! + XCTAssertFalse( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } + } + + func testWhenURLHostEndingMatchesAnyUserDefinedHost_itIsConsideredFirstParty() { + // NOTE: RUMM-722 why that for loop here? https://github.com/DataDog/dd-sdk-ios/pull/384 + for _ in 0...5 { + let filter = FirstPartyURLsFilter( + hosts: ["first-party.com": .init(.dd), "eu": .init(.dd)] + ) + fixtures1stParty.forEach { fixture in + let url = URL(string: fixture)! + XCTAssertTrue( + filter.isFirstParty(url: url), + "The url: `\(url)` should be matched as first party." + ) + } + } + } + + func testWhenURLHostDoesNotMatchEndingOfAnyOfUserDefinedHosts_itIsNotConsideredFirstParty() { + // NOTE: RUMM-722 why that for loop here? https://github.com/DataDog/dd-sdk-ios/pull/384 + for _ in 0...5 { + let filter = FirstPartyURLsFilter( + hosts: ["first-party.com": .init(.dd), "eu": .init(.b3m)] + ) + fixtures3rdParty.forEach { fixture in + let url = URL(string: fixture)! + XCTAssertFalse( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } + } + } + + func testWhenURLHostIsSubdomain_itIsConsideredFirstParty() { + let filter = FirstPartyURLsFilter( + hosts: ["first-party.com": .init(.dd)] + ) + let url = URL(string: "https://api.first-party.com")! + XCTAssertTrue( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } + + func testWhenURLHostIsNotSubdomain_itIsNotConsideredFirstParty() { + let filter = FirstPartyURLsFilter( + hosts: ["first-party.com": .init(.dd)] + ) + let url = URL(string: "https://apifirst-party.com")! + XCTAssertFalse( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } +} diff --git a/DatadogCore/Tests/Datadog/Utils/Casting+Tracing.swift b/DatadogCore/Tests/Datadog/Utils/Casting+Tracing.swift new file mode 100644 index 0000000000..538ce9d4f6 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Utils/Casting+Tracing.swift @@ -0,0 +1,27 @@ +/* + * 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. + */ + +@testable import DatadogTrace + +/* + NOTE: The casting methods defined here do shadow the ones defined in `Datadog.Casting`. + The difference is that here in tests we do force unwrapping (`as!`), whereas in `Datadog` we do `as?` with a warning. + + This is needed for expressiveness in testing, where i.e. `XCTAssertNil(span.context.dd?.parentID)` may give a false positive + without considering if the `parentID` is `nil`. Using `span.context.dd.parentID` mitigates it. + */ + +internal extension OTTracer { + var dd: DatadogTracer { self as! DatadogTracer } +} + +internal extension OTSpan { + var dd: DDSpan { self as! DDSpan } +} + +internal extension OTSpanContext { + var dd: DDSpanContext { self as! DDSpanContext } +} diff --git a/DatadogCore/Tests/Datadog/Utils/SwiftUIExtensionsTests.swift b/DatadogCore/Tests/Datadog/Utils/SwiftUIExtensionsTests.swift new file mode 100644 index 0000000000..494cce1b28 --- /dev/null +++ b/DatadogCore/Tests/Datadog/Utils/SwiftUIExtensionsTests.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. + */ + +#if canImport(SwiftUI) + +import XCTest +import SwiftUI + +@testable import DatadogRUM +@testable import DatadogCore + +@available(iOS 13, tvOS 13, *) +class CustomHostingController: UIHostingController {} + +@available(iOS 13, tvOS 13, *) +final class TestView: View { + var body = EmptyView() +} + +class SwiftUIExtensionsTests: XCTestCase { + func testSwiftUIViewTypeDescription() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + + let view = TestView().cornerRadius(8) + XCTAssertEqual(view.typeDescription, "ModifiedContent>") + } + + func testBundleIsSwiftUI() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + + // Given + let someSwiftUITypes: [AnyClass] = [ + UIHostingController.self // The only class in SwiftUI + ] + + let someNonSwiftUITypes: [AnyClass] = [ + TestView.self, + UIViewController.self, + OperationQueue.self, + ] + + // Then + someSwiftUITypes.forEach { XCTAssertTrue(Bundle(for: $0).isSwiftUI) } + someNonSwiftUITypes.forEach { XCTAssertFalse(Bundle(for: $0).isSwiftUI) } + } +} +#endif diff --git a/DatadogCore/Tests/Datadog/Utils/UIKitExtensionsTests.swift b/DatadogCore/Tests/Datadog/Utils/UIKitExtensionsTests.swift new file mode 100644 index 0000000000..6c146223ec --- /dev/null +++ b/DatadogCore/Tests/Datadog/Utils/UIKitExtensionsTests.swift @@ -0,0 +1,43 @@ +/* + * 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 UIKit +@testable import DatadogRUM + +class CustomSwiftViewController: UIViewController {} + +class UIKitExtensionsTests: XCTestCase { + func testViewControllerCanonicalClassName() { + let swiftViewController = CustomSwiftViewController() + let objcViewController = CustomObjcViewController() + + #if os(iOS) + XCTAssertEqual(swiftViewController.canonicalClassName, "DatadogCoreTests_iOS.CustomSwiftViewController") + #elseif os(tvOS) + XCTAssertEqual(swiftViewController.canonicalClassName, "DatadogCoreTests_tvOS.CustomSwiftViewController") + #endif + XCTAssertEqual(objcViewController.canonicalClassName, "CustomObjcViewController") + } + + func testBundleIsUIKit() { + let someUIKitClasses: [AnyClass] = [ + UIViewController.self, + UIButton.self, + UINavigationBar.self, + UIScrollView.self + ] + + let someNonUIKitClasses: [AnyClass] = [ + CustomSwiftViewController.self, + CustomObjcViewController.self, + OperationQueue.self, + ] + + someUIKitClasses.forEach { XCTAssertTrue(Bundle(for: $0).isUIKit) } + someNonUIKitClasses.forEach { XCTAssertFalse(Bundle(for: $0).isUIKit) } + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift new file mode 100644 index 0000000000..18ee3cb215 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDConfigurationTests.swift @@ -0,0 +1,125 @@ +/* +* 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 DatadogRUM + +@testable import DatadogCore +@testable import DatadogObjc + +/// This tests verify that objc-compatible `DatadogObjc` wrapper properly interacts with`Datadog` public API (swift). +class DDConfigurationTests: XCTestCase { + func testDefaultBuilderForwardsInitializationToSwift() throws { + let objcConfig = DDConfiguration(clientToken: "abc-123", env: "tests") + XCTAssertEqual(objcConfig.sdkConfiguration.clientToken, "abc-123") + XCTAssertEqual(objcConfig.sdkConfiguration.site, .us1) + XCTAssertEqual(objcConfig.sdkConfiguration.env, "tests") + XCTAssertNil(objcConfig.sdkConfiguration.service) + XCTAssertEqual(objcConfig.sdkConfiguration.batchSize, .medium) + XCTAssertEqual(objcConfig.sdkConfiguration.uploadFrequency, .average) + XCTAssertEqual(objcConfig.sdkConfiguration.additionalConfiguration.count, 0) + XCTAssertNil(objcConfig.sdkConfiguration.encryption) + XCTAssertNotNil(objcConfig.sdkConfiguration.serverDateProvider) + XCTAssertFalse(objcConfig.sdkConfiguration.backgroundTasksEnabled) + } + + func testCustomizedBuilderForwardsInitializationToSwift() throws { + let objcConfig = DDConfiguration(clientToken: "abc-123", env: "tests") + + objcConfig.site = .eu1() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .eu1) + + objcConfig.site = .ap1() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .ap1) + + objcConfig.site = .us1() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .us1) + + objcConfig.site = .us3() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .us3) + + objcConfig.site = .us5() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .us5) + + objcConfig.site = .us1_fed() + XCTAssertEqual(objcConfig.sdkConfiguration.site, .us1_fed) + + objcConfig.service = "service-name" + XCTAssertEqual(objcConfig.sdkConfiguration.service, "service-name") + + objcConfig.batchSize = .small + XCTAssertEqual(objcConfig.sdkConfiguration.batchSize, .small) + + objcConfig.batchSize = .large + XCTAssertEqual(objcConfig.sdkConfiguration.batchSize, .large) + + objcConfig.uploadFrequency = .frequent + XCTAssertEqual(objcConfig.sdkConfiguration.uploadFrequency, .frequent) + + objcConfig.uploadFrequency = .rare + XCTAssertEqual(objcConfig.sdkConfiguration.uploadFrequency, .rare) + + objcConfig.batchProcessingLevel = .low + XCTAssertEqual(objcConfig.sdkConfiguration.batchProcessingLevel, .low) + + objcConfig.batchProcessingLevel = .high + XCTAssertEqual(objcConfig.sdkConfiguration.batchProcessingLevel, .high) + + objcConfig.proxyConfiguration = [kCFNetworkProxiesHTTPEnable: true, kCFNetworkProxiesHTTPPort: 123, kCFNetworkProxiesHTTPProxy: "www.example.com", kCFProxyUsernameKey: "proxyuser", kCFProxyPasswordKey: "proxypass" ] + objcConfig.additionalConfiguration = ["additional": "config"] + + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPEnable] as? Bool, true) + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPPort] as? Int, 123) + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFNetworkProxiesHTTPProxy] as? String, "www.example.com") + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFProxyUsernameKey] as? String, "proxyuser") + XCTAssertEqual(objcConfig.sdkConfiguration.proxyConfiguration?[kCFProxyPasswordKey] as? String, "proxypass") + XCTAssertEqual(objcConfig.sdkConfiguration._internal.additionalConfiguration["additional"] as? String, "config") + + class ObjCDataEncryption: DDDataEncryption { + func encrypt(data: Data) throws -> Data { data } + func decrypt(data: Data) throws -> Data { data } + } + let dataEncryption = ObjCDataEncryption() + objcConfig.setEncryption(dataEncryption) + XCTAssertTrue((objcConfig.sdkConfiguration.encryption as? DDDataEncryptionBridge)?.objcEncryption === dataEncryption) + + class ObjcServerDateProvider: DDServerDateProvider { + func synchronize(update: @escaping (TimeInterval) -> Void) { } + } + 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 { + // Given + class ObjCDataEncryption: DDDataEncryption { + let encData: Data = .mockRandom() + let decData: Data = .mockRandom() + func encrypt(data: Data) throws -> Data { encData } + func decrypt(data: Data) throws -> Data { decData } + } + + let encryption = ObjCDataEncryption() + + // When + let objcConfig = DDConfiguration( + clientToken: "abc-123", + env: "tests" + ) + objcConfig.setEncryption(encryption) + let configuration = objcConfig.sdkConfiguration + + // Then + XCTAssertEqual(try configuration.encryption?.encrypt(data: .mockRandom()), encryption.encData) + XCTAssertEqual(try configuration.encryption?.decrypt(data: .mockRandom()), encryption.decData) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift new file mode 100644 index 0000000000..1f7e7f7d87 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDDatadogTests.swift @@ -0,0 +1,192 @@ +/* +* 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 DatadogLogs +@testable import DatadogCore +@testable import DatadogObjc + +/// This tests verify that objc-compatible `DatadogObjc` wrapper properly interacts with`Datadog` public API (swift). +class DDDatadogTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertFalse(Datadog.isInitialized()) + } + + override func tearDown() { + XCTAssertFalse(Datadog.isInitialized()) + super.tearDown() + } + + // MARK: - SDK initialization / stop lifecycle + + func testItForwardsInitializationToSwift() throws { + let config = DDConfiguration( + clientToken: "abcefghi", + env: "tests" + ) + + config.bundle = .mockWith(CFBundleExecutable: "app-name") + + DDDatadog.initialize( + configuration: config, + trackingConsent: randomConsent().objc + ) + + XCTAssertTrue(Datadog.isInitialized()) + + let context = try XCTUnwrap(CoreRegistry.default as? DatadogCore).contextProvider.read() + XCTAssertEqual(context.applicationName, "app-name") + XCTAssertEqual(context.env, "tests") + + Datadog.flushAndDeinitialize() + + XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) + } + + func testItReflectsInitializationStatus() throws { + let config = DDConfiguration( + clientToken: "abcefghi", + env: "tests" + ) + + config.bundle = .mockWith(CFBundleExecutable: "app-name") + XCTAssertFalse(DDDatadog.isInitialized()) + + DDDatadog.initialize( + configuration: config, + trackingConsent: randomConsent().objc + ) + + XCTAssertTrue(DDDatadog.isInitialized()) + + Datadog.flushAndDeinitialize() + + XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) + } + + func testItForwardsStopInstanceToSwift() throws { + let config = DDConfiguration( + clientToken: "abcefghi", + env: "tests" + ) + + config.bundle = .mockWith(CFBundleExecutable: "app-name") + + DDDatadog.initialize( + configuration: config, + trackingConsent: randomConsent().objc + ) + + XCTAssertTrue(Datadog.isInitialized()) + + DDDatadog.stopInstance() + + XCTAssertFalse(Datadog.isInitialized()) + + XCTAssertNil(CoreRegistry.default.get(feature: LogsFeature.self)) + } + + // MARK: - Changing Tracking Consent + + func testItForwardsTrackingConsentToSwift() { + let initialConsent = randomConsent() + let nextConsent = randomConsent() + + DDDatadog.initialize( + configuration: DDConfiguration(clientToken: "abcefghi", env: "tests"), + trackingConsent: initialConsent.objc + ) + + let core = CoreRegistry.default as? DatadogCore + XCTAssertEqual(core?.consentPublisher.consent, initialConsent.swift) + + DDDatadog.setTrackingConsent(consent: nextConsent.objc) + + XCTAssertEqual(core?.consentPublisher.consent, nextConsent.swift) + + Datadog.flushAndDeinitialize() + } + + // MARK: - Setting user info + + func testItForwardsUserInfoToSwift() throws { + DDDatadog.initialize( + configuration: DDConfiguration(clientToken: "abcefghi", env: "tests"), + trackingConsent: randomConsent().objc + ) + + let core = CoreRegistry.default as? DatadogCore + let userInfo = try XCTUnwrap(core?.userInfoPublisher) + + DDDatadog.setUserInfo( + id: "id", + name: "name", + email: "email", + extraInfo: [ + "attribute-int": 42, + "attribute-double": 42.5, + "attribute-string": "string value" + ] + ) + DDDatadog.addUserExtraInfo(["foo": "bar"]) + XCTAssertEqual(userInfo.current.id, "id") + XCTAssertEqual(userInfo.current.name, "name") + XCTAssertEqual(userInfo.current.email, "email") + 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) + XCTAssertNil(userInfo.current.name) + XCTAssertNil(userInfo.current.email) + XCTAssertTrue(userInfo.current.extraInfo.isEmpty) + + Datadog.flushAndDeinitialize() + } + + // MARK: - Changing SDK verbosity level + + private let swiftVerbosityLevels: [CoreLoggerLevel?] = [ + .debug, .warn, .error, .critical, nil + ] + private let objcVerbosityLevels: [DDSDKVerbosityLevel] = [ + .debug, .warn, .error, .critical, .none + ] + + func testItForwardsSettingVerbosityLevelToSwift() { + defer { Datadog.verbosityLevel = nil } + + zip(swiftVerbosityLevels, objcVerbosityLevels).forEach { swiftLevel, objcLevel in + DDDatadog.setVerbosityLevel(objcLevel) + XCTAssertEqual(Datadog.verbosityLevel, swiftLevel) + } + } + + func testItGetsVerbosityLevelFromSwift() { + defer { Datadog.verbosityLevel = nil } + + zip(swiftVerbosityLevels, objcVerbosityLevels).forEach { swiftLevel, objcLevel in + Datadog.verbosityLevel = swiftLevel + XCTAssertEqual(DDDatadog.verbosityLevel(), objcLevel) + } + } + + // MARK: - Helpers + + private func randomConsent() -> (objc: DDTrackingConsent, swift: TrackingConsent) { + let objcConsents: [DDTrackingConsent] = [.granted(), .notGranted(), .pending()] + let swiftConsents: [TrackingConsent] = [.granted, .notGranted, .pending] + let index: Int = .random(in: 0..<3) + return (objc: objcConsents[index], swift: swiftConsents[index]) + } +} 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 new file mode 100644 index 0000000000..f4a5eac7a7 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDLogsTests.swift @@ -0,0 +1,294 @@ +/* +* 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 +import TestUtilities + +@testable import DatadogLogs +@testable import DatadogCore +@testable import DatadogObjc + +// swiftlint:disable multiline_arguments_brackets +// swiftlint:disable compiler_protocol_init +class DDLogsTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + CoreRegistry.register(default: core) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() + core.flushAndTearDown() + core = nil + super.tearDown() + } + + func testDefaultConfiguration() { + // Given + let config = DDLogsConfiguration() + + // Then + XCTAssertNil(config.configuration.customEndpoint) + } + + func testConfigurationOverrides() throws { + // Given + let customEndpoint: URL = .mockRandom() + + // When + DDLogs.enable( + with: DDLogsConfiguration( + customEndpoint: customEndpoint + ) + ) + + // Then + let logs = try XCTUnwrap(core.get(feature: LogsFeature.self)) + let requestBuilder = try XCTUnwrap(logs.requestBuilder as? RequestBuilder) + XCTAssertEqual(requestBuilder.customIntakeURL, customEndpoint) + } + + func testAddGlobalAttributes() throws { + // Given + DDLogs.enable() + + DDLogs.addAttribute(forKey: "nsstring", value: NSString(string: "hello")) + DDLogs.addAttribute(forKey: "nsbool", value: NSNumber(booleanLiteral: true)) + DDLogs.addAttribute(forKey: "nsint", value: NSInteger(integerLiteral: 10)) + DDLogs.addAttribute(forKey: "nsnumber", value: NSNumber(value: 10.5)) + DDLogs.addAttribute(forKey: "nsnull", value: NSNull()) + DDLogs.addAttribute(forKey: "nsurl", value: NSURL(string: "http://apple.com")!) + DDLogs.addAttribute( + forKey: "nsarray-of-int", + value: NSArray(array: [1, 2, 3]) + ) + DDLogs.addAttribute( + forKey: "nsdictionary-of-date", + value: NSDictionary(dictionary: [ + "date1": Date.mockDecember15th2019At10AMUTC(), + "date2": Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 60 * 60) + ]) + ) + + // When + let objcLogger = DDLogger.create() + objcLogger.info("message") + + // Then + let logMatcher = try core.waitAndReturnLogMatchers()[0] + logMatcher.assertValue(forKey: "nsstring", equals: "hello") + logMatcher.assertValue(forKey: "nsbool", equals: true) + logMatcher.assertValue(forKey: "nsint", equals: 10) + logMatcher.assertValue(forKey: "nsnumber", equals: 10.5) + logMatcher.assertValue(forKeyPath: "nsnull", isTypeOf: Optional.self) + logMatcher.assertValue(forKey: "nsurl", equals: "http://apple.com") + logMatcher.assertValue(forKey: "nsarray-of-int", equals: [1, 2, 3]) + logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date1", equals: "2019-12-15T10:00:00.000Z") + logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date2", equals: "2019-12-15T11:00:00.000Z") + } + + func testRemoveGlobalAttributes() throws { + // Given + DDLogs.enable() + + DDLogs.addAttribute(forKey: "custom-attribute", value: NSString(string: "custom-value")) + DDLogs.removeAttribute(forKey: "custom-attribute") + + // When + let objcLogger = DDLogger.create() + objcLogger.info("message") + + // Then + let logMatcher = try core.waitAndReturnLogMatchers()[0] + logMatcher.assertNoValue(forKey: "custom-attribute") + } + + func testSendingLogsWithDifferentLevels() throws { + let feature: LogsFeature = .mockAny() + try CoreRegistry.default.register(feature: feature) + + let objcLogger = DDLogger.create() + + objcLogger.debug("message") + objcLogger.info("message") + objcLogger.notice("message") + objcLogger.warn("message") + objcLogger.error("message") + objcLogger.critical("message") + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") + } + + func testSendingNSError() throws { + let feature: LogsFeature = .mockAny() + try CoreRegistry.default.register(feature: feature) + + let objcLogger = DDLogger.create() + + let error = NSError(domain: "UnitTest", code: 11_235, userInfo: [NSLocalizedDescriptionKey: "UnitTest error"]) + + objcLogger.debug("message", error: error, attributes: [:]) + objcLogger.info("message", error: error, attributes: [:]) + objcLogger.notice("message", error: error, attributes: [:]) + objcLogger.warn("message", error: error, attributes: [:]) + objcLogger.error("message", error: error, attributes: [:]) + objcLogger.critical("message", error: error, attributes: [:]) + + let logMatchers = try core.waitAndReturnLogMatchers() + for matcher in logMatchers { + matcher.assertValue( + forKeyPath: "error.stack", + equals: "Error Domain=UnitTest Code=11235 \"UnitTest error\" UserInfo={NSLocalizedDescription=UnitTest error}" + ) + matcher.assertValue( + forKeyPath: "error.message", + equals: "UnitTest error" + ) + matcher.assertValue( + forKeyPath: "error.kind", + equals: "UnitTest - 11235" + ) + } + } + + func testSendingMessageAttributes() throws { + let feature: LogsFeature = .mockAny() + try CoreRegistry.default.register(feature: feature) + + let objcLogger = DDLogger.create() + + objcLogger.debug("message", attributes: ["foo": "bar"]) + objcLogger.info("message", attributes: ["foo": "bar"]) + objcLogger.notice("message", attributes: ["foo": "bar"]) + objcLogger.warn("message", attributes: ["foo": "bar"]) + objcLogger.error("message", attributes: ["foo": "bar"]) + objcLogger.critical("message", attributes: ["foo": "bar"]) + + let logMatchers = try core.waitAndReturnLogMatchers() + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") + logMatchers.forEach { matcher in + matcher.assertAttributes(equal: ["foo": "bar"]) + } + } + + func testSendingLoggerAttributes() throws { + let feature: LogsFeature = .mockAny() + try CoreRegistry.default.register(feature: feature) + + let objcLogger = DDLogger.create() + + objcLogger.addAttribute(forKey: "nsstring", value: NSString(string: "hello")) + objcLogger.addAttribute(forKey: "nsbool", value: NSNumber(booleanLiteral: true)) + objcLogger.addAttribute(forKey: "nsint", value: NSInteger(integerLiteral: 10)) + objcLogger.addAttribute(forKey: "nsnumber", value: NSNumber(value: 10.5)) + objcLogger.addAttribute(forKey: "nsnull", value: NSNull()) + objcLogger.addAttribute(forKey: "nsurl", value: NSURL(string: "http://apple.com")!) + objcLogger.addAttribute( + forKey: "nsarray-of-int", + value: NSArray(array: [1, 2, 3]) + ) + objcLogger.addAttribute( + forKey: "nsdictionary-of-date", + value: NSDictionary(dictionary: [ + "date1": Date.mockDecember15th2019At10AMUTC(), + "date2": Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 60 * 60) + ]) + ) + objcLogger.info("message") + + let logMatcher = try core.waitAndReturnLogMatchers()[0] + logMatcher.assertValue(forKey: "nsstring", equals: "hello") + logMatcher.assertValue(forKey: "nsbool", equals: true) + logMatcher.assertValue(forKey: "nsint", equals: 10) + logMatcher.assertValue(forKey: "nsnumber", equals: 10.5) + logMatcher.assertValue(forKeyPath: "nsnull", isTypeOf: Optional.self) + logMatcher.assertValue(forKey: "nsurl", equals: "http://apple.com") + logMatcher.assertValue(forKey: "nsarray-of-int", equals: [1, 2, 3]) + logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date1", equals: "2019-12-15T10:00:00.000Z") + logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date2", equals: "2019-12-15T11:00:00.000Z") + } + + func testSettingTagsAndAttributes() throws { + core.context = .mockWith( + env: "test", + version: "1.2.3" + ) + + let feature: LogsFeature = .mockAny() + try CoreRegistry.default.register(feature: feature) + + let objcLogger = DDLogger.create() + + objcLogger.addAttribute(forKey: "foo", value: "bar") + objcLogger.addAttribute(forKey: "bizz", value: "buzz") + objcLogger.removeAttribute(forKey: "bizz") + + objcLogger.addTag(withKey: "foo", value: "bar") + objcLogger.addTag(withKey: "bizz", value: "buzz") + objcLogger.removeTag(withKey: "bizz") + + objcLogger.add(tag: "foobar") + objcLogger.add(tag: "bizzbuzz") + objcLogger.remove(tag: "bizzbuzz") + + objcLogger.info(.mockAny()) + + let logMatcher = try core.waitAndReturnLogMatchers()[0] + logMatcher.assertValue(forKeyPath: "foo", equals: "bar") + logMatcher.assertNoValue(forKey: "bizz") + logMatcher.assertTags(equal: ["foo:bar", "foobar", "env:test", "version:1.2.3"]) + } + + func testItForwardsLoggerConfigurationToSwift() { + let objcConfig = DDLoggerConfiguration() + objcConfig.name = "logger-name" + objcConfig.service = "service-name" + objcConfig.networkInfoEnabled = true + objcConfig.remoteSampleRate = 50 + objcConfig.printLogsToConsole = true + + XCTAssertEqual(objcConfig.configuration.name, "logger-name") + XCTAssertEqual(objcConfig.configuration.service, "service-name") + XCTAssertTrue(objcConfig.configuration.networkInfoEnabled) + 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/DDNSURLSessionDelegateTests.swift b/DatadogCore/Tests/DatadogObjc/DDNSURLSessionDelegateTests.swift new file mode 100644 index 0000000000..ce6292e4f3 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDNSURLSessionDelegateTests.swift @@ -0,0 +1,66 @@ +/* + * 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 +@testable import DatadogCore +@testable import DatadogObjc + +@available(*, deprecated) +private class DDURLSessionDelegateMock: DDURLSessionDelegate { + var calledDidFinishCollecting = false + var calledDidCompleteWithError = false + var calledDidReceiveData = false + + override func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + calledDidFinishCollecting = true + } + + override func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + calledDidCompleteWithError = true + } + + override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + calledDidReceiveData = true + } +} + +@available(*, deprecated) +class DDNSURLSessionDelegateTests: XCTestCase { + private var core: FeatureRegistrationCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + + core = FeatureRegistrationCoreMock() + CoreRegistry.register(default: core) + + let config = DDRUMConfiguration(applicationID: "fake-id") + config.setURLSessionTracking(.init()) + DDRUM.enable(with: config) + } + + override func tearDown() { + DDURLSessionInstrumentation.disable(delegateClass: DDNSURLSessionDelegate.self) + CoreRegistry.unregisterDefault() + core = nil + + super.tearDown() + } + + func testInit() { + _ = DDNSURLSessionDelegate() + } + + func testInitWithAdditionalFirstPartyHostsWithHeaderTypes() { + _ = DDNSURLSessionDelegate(additionalFirstPartyHostsWithHeaderTypes: ["foo.com": [.datadog]]) + } + + func testInitWithAdditionalFirstPartyHosts() { + _ = DDNSURLSessionDelegate(additionalFirstPartyHosts: ["foo.com"]) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift new file mode 100644 index 0000000000..6468bbe483 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDRUMConfigurationTests.swift @@ -0,0 +1,199 @@ +/* + * 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 DatadogRUM +@testable import DatadogObjc + +class DDRUMConfigurationTests: XCTestCase { + private var objc = DDRUMConfiguration(applicationID: "app-id") + private var swift: RUM.Configuration { objc.swiftConfig } + + func testApplicationID() { + objc = DDRUMConfiguration(applicationID: "rum-app-id") + XCTAssertEqual(swift.applicationID, "rum-app-id") + } + + func testSessionSampleRate() { + objc.sessionSampleRate = 30 + XCTAssertEqual(objc.sessionSampleRate, 30) + XCTAssertEqual(swift.sessionSampleRate, 30) + } + + func testTelemetrySampleRate() { + objc.telemetrySampleRate = 30 + XCTAssertEqual(objc.telemetrySampleRate, 30) + XCTAssertEqual(swift.telemetrySampleRate, 30) + } + + func testUIKitViewsPredicate() { + class ObjcPredicate: DDUIKitRUMViewsPredicate { + func rumView(for viewController: UIViewController) -> DDRUMView? { nil } + } + let predicate = ObjcPredicate() + objc.uiKitViewsPredicate = predicate + XCTAssertIdentical(objc.uiKitViewsPredicate, predicate) + XCTAssertNotNil(swift.uiKitViewsPredicate) + } + + func testUIKitActionsPredicate() { + class ObjcPredicate: DDUIKitRUMActionsPredicate & DDUITouchRUMActionsPredicate & DDUIPressRUMActionsPredicate { + func rumAction(targetView: UIView) -> DDRUMAction? { nil } + func rumAction(press type: UIPress.PressType, targetView: UIView) -> DDRUMAction? { nil } + } + let predicate = ObjcPredicate() + objc.uiKitActionsPredicate = predicate + XCTAssertIdentical(objc.uiKitActionsPredicate, predicate) + XCTAssertNotNil(swift.uiKitActionsPredicate) + } + + func testSetDDRUMURLSessionTrackingWithFirstPartyHosts() { + let tracking = DDRUMURLSessionTracking() + + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, RUM.Configuration.URLSessionTracking()) + + tracking.setFirstPartyHostsTracing(.init(hosts: ["foo.com"])) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .trace(hosts: ["foo.com"]))) + + tracking.setFirstPartyHostsTracing(.init(hosts: ["foo.com"], sampleRate: 99)) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: 99))) + + tracking.setFirstPartyHostsTracing(.init(hostsWithHeaderTypes: ["foo.com": [.b3, .datadog]])) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: ["foo.com": [.b3, .datadog]]))) + + tracking.setFirstPartyHostsTracing(.init(hostsWithHeaderTypes: ["foo.com": [.b3, .datadog]], sampleRate: 99)) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: ["foo.com": [.b3, .datadog]], sampleRate: 99))) + } + + func testSetDDRUMURLSessionTrackingWithResourceAttributesProvider() { + let tracking = DDRUMURLSessionTracking() + + objc.setURLSessionTracking(tracking) + XCTAssertNil(swift.urlSessionTracking?.resourceAttributesProvider) + + tracking.setResourceAttributesProvider { _, _, _, _ in nil } + objc.setURLSessionTracking(tracking) + XCTAssertNotNil(swift.urlSessionTracking?.resourceAttributesProvider) + } + + func testFrustrationsTracking() { + let random: Bool = .mockRandom() + objc.trackFrustrations = random + XCTAssertEqual(objc.trackFrustrations, random) + XCTAssertEqual(swift.trackFrustrations, random) + } + + func testBackgroundEventsTracking() { + let random: Bool = .mockRandom() + objc.trackBackgroundEvents = random + XCTAssertEqual(objc.trackBackgroundEvents, random) + XCTAssertEqual(swift.trackBackgroundEvents, random) + } + + func testLongTaskThreshold() { + let random: TimeInterval = .mockRandom() + objc.longTaskThreshold = random + XCTAssertEqual(objc.longTaskThreshold, random) + XCTAssertEqual(swift.longTaskThreshold, random) + } + + func testAppHangThreshold() { + 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) + + objc.vitalsUpdateFrequency = .never + XCTAssertNil(swift.vitalsUpdateFrequency) + } + + func testEventMappers() { + let swiftViewEvent: RUMViewEvent = .mockRandom() + let swiftResourceEvent: RUMResourceEvent = .mockRandom() + let swiftActionEvent: RUMActionEvent = .mockRandom() + let swiftErrorEvent: RUMErrorEvent = .mockRandom() + let swiftLongTaskEvent: RUMLongTaskEvent = .mockRandom() + + objc.setViewEventMapper { objcViewEvent in + DDAssertReflectionEqual(objcViewEvent.swiftModel, swiftViewEvent) + objcViewEvent.view.url = "redacted view.url" + return objcViewEvent + } + + objc.setResourceEventMapper { objcResourceEvent in + DDAssertReflectionEqual(objcResourceEvent.swiftModel, swiftResourceEvent) + objcResourceEvent.view.url = "redacted view.url" + objcResourceEvent.resource.url = "redacted resource.url" + return objcResourceEvent + } + + objc.setActionEventMapper { objcActionEvent in + DDAssertReflectionEqual(objcActionEvent.swiftModel, swiftActionEvent) + objcActionEvent.view.url = "redacted view.url" + objcActionEvent.action.target?.name = "redacted action.target.name" + return objcActionEvent + } + + objc.setErrorEventMapper { objcErrorEvent in + DDAssertReflectionEqual(objcErrorEvent.swiftModel, swiftErrorEvent) + objcErrorEvent.view.url = "redacted view.url" + objcErrorEvent.error.message = "redacted error.message" + objcErrorEvent.error.resource?.url = "redacted error.resource.url" + return objcErrorEvent + } + + objc.setLongTaskEventMapper { objcLongTaskEvent in + DDAssertReflectionEqual(objcLongTaskEvent.swiftModel, swiftLongTaskEvent) + objcLongTaskEvent.view.url = "redacted view.url" + return objcLongTaskEvent + } + + let redactedSwiftViewEvent = swift.viewEventMapper?(swiftViewEvent) + let redactedSwiftResourceEvent = swift.resourceEventMapper?(swiftResourceEvent) + let redactedSwiftActionEvent = swift.actionEventMapper?(swiftActionEvent) + let redactedSwiftErrorEvent = swift.errorEventMapper?(swiftErrorEvent) + let redactedSwiftLongTaskEvent = swift.longTaskEventMapper?(swiftLongTaskEvent) + + XCTAssertEqual(redactedSwiftViewEvent?.view.url, "redacted view.url") + XCTAssertEqual(redactedSwiftResourceEvent?.view.url, "redacted view.url") + XCTAssertEqual(redactedSwiftResourceEvent?.resource.url, "redacted resource.url") + XCTAssertEqual(redactedSwiftActionEvent?.view.url, "redacted view.url") + XCTAssertEqual(redactedSwiftActionEvent?.action.target?.name, "redacted action.target.name") + XCTAssertEqual(redactedSwiftErrorEvent?.view.url, "redacted view.url") + XCTAssertEqual(redactedSwiftErrorEvent?.error.message, "redacted error.message") + XCTAssertEqual(redactedSwiftErrorEvent?.error.resource?.url, "redacted error.resource.url") + XCTAssertEqual(redactedSwiftLongTaskEvent?.view.url, "redacted view.url") + } + + func testOnSessionStart() { + objc.onSessionStart = { _, _ in } + XCTAssertNotNil(swift.onSessionStart) + } + + func testCustomEndpoint() { + let random: URL = .mockRandom() + objc.customEndpoint = random + XCTAssertEqual(objc.customEndpoint, random) + XCTAssertEqual(swift.customEndpoint, random) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift new file mode 100644 index 0000000000..059b376c1a --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDRUMMonitorTests.swift @@ -0,0 +1,465 @@ +/* + * 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 +@testable import DatadogRUM +@testable import DatadogCore +@testable import DatadogObjc + +class UIKitRUMViewsPredicateBridgeTests: XCTestCase { + func testItForwardsCallToObjcPredicate() { + class MockPredicate: DDUIKitRUMViewsPredicate { + var didCallRUMView = false + func rumView(for viewController: UIViewController) -> DDRUMView? { + didCallRUMView = true + return nil + } + } + + let objcPredicate = MockPredicate() + + let predicateBridge = UIKitRUMViewsPredicateBridge(objcPredicate: objcPredicate) + _ = predicateBridge.rumView(for: mockView) + + XCTAssertTrue(objcPredicate.didCallRUMView) + } +} + +class DDRUMViewTests: XCTestCase { + func testItCreatesSwiftRUMView() { + let objcRUMView = DDRUMView(name: "name", attributes: ["foo": "bar"]) + XCTAssertEqual(objcRUMView.swiftView.name, "name") + XCTAssertEqual(objcRUMView.swiftView.attributes["foo"]?.dd.decode(), "bar") + XCTAssertEqual(objcRUMView.name, "name") + XCTAssertEqual(objcRUMView.attributes["foo"] as? String, "bar") + } +} + +class UIKitRUMActionsPredicateBridgeTests: XCTestCase { + func testItForwardsCallToObjcTouchPredicate() { + class MockPredicate: DDUITouchRUMActionsPredicate { + var didCallRUMAction = false + func rumAction(targetView: UIView) -> DDRUMAction? { + didCallRUMAction = true + return nil + } + } + + let objcPredicate = MockPredicate() + + let predicateBridge = UIKitRUMActionsPredicateBridge(objcPredicate: objcPredicate) + _ = predicateBridge.rumAction(targetView: UIView()) + + XCTAssertTrue(objcPredicate.didCallRUMAction) + } + + func testItForwardsCallToObjcPressPredicate() { + class MockPredicate: DDUIPressRUMActionsPredicate { + var didCallRUMAction = false + func rumAction(press: UIPress.PressType, targetView: UIView) -> DDRUMAction? { + didCallRUMAction = true + return nil + } + } + + let objcPredicate = MockPredicate() + + let predicateBridge = UIKitRUMActionsPredicateBridge(objcPredicate: objcPredicate) + _ = predicateBridge.rumAction(press: .select, targetView: UIView()) + + XCTAssertTrue(objcPredicate.didCallRUMAction) + } +} + +class DDRUMActionTests: XCTestCase { + func testItCreatesSwiftRUMAction() { + let objcRUMAction = DDRUMAction(name: "name", attributes: ["foo": "bar"]) + XCTAssertEqual(objcRUMAction.swiftAction.name, "name") + XCTAssertEqual(objcRUMAction.swiftAction.attributes["foo"]?.dd.decode(), "bar") + XCTAssertEqual(objcRUMAction.name, "name") + XCTAssertEqual(objcRUMAction.attributes["foo"] as? String, "bar") + } +} + +class DDRUMUserActionTypeTests: XCTestCase { + func testMappingToSwiftRUMActionType() { + XCTAssertEqual(DDRUMActionType.tap.swiftType, .tap) + XCTAssertEqual(DDRUMActionType.scroll.swiftType, .scroll) + XCTAssertEqual(DDRUMActionType.swipe.swiftType, .swipe) + XCTAssertEqual(DDRUMActionType.custom.swiftType, .custom) + } +} + +class DDRUMErrorSourceTests: XCTestCase { + func testMappingToSwiftRUMErrorSource() { + XCTAssertEqual(DDRUMErrorSource.source.swiftType, .source) + XCTAssertEqual(DDRUMErrorSource.network.swiftType, .network) + XCTAssertEqual(DDRUMErrorSource.webview.swiftType, .webview) + XCTAssertEqual(DDRUMErrorSource.console.swiftType, .console) + XCTAssertEqual(DDRUMErrorSource.custom.swiftType, .custom) + } +} + +class DDRUMResourceKindTests: XCTestCase { + func testMappingToSwiftRUMResourceKind() { + XCTAssertEqual(DDRUMResourceType.image.swiftType, .image) + XCTAssertEqual(DDRUMResourceType.xhr.swiftType, .xhr) + XCTAssertEqual(DDRUMResourceType.beacon.swiftType, .beacon) + XCTAssertEqual(DDRUMResourceType.css.swiftType, .css) + XCTAssertEqual(DDRUMResourceType.document.swiftType, .document) + XCTAssertEqual(DDRUMResourceType.fetch.swiftType, .fetch) + XCTAssertEqual(DDRUMResourceType.font.swiftType, .font) + XCTAssertEqual(DDRUMResourceType.js.swiftType, .js) + XCTAssertEqual(DDRUMResourceType.media.swiftType, .media) + XCTAssertEqual(DDRUMResourceType.other.swiftType, .other) + XCTAssertEqual(DDRUMResourceType.native.swiftType, .native) + } +} + +class DDRUMMethodTests: XCTestCase { + func testMappingToSwiftRUMMethod() { + XCTAssertEqual(DDRUMMethod.post.swiftType, .post) + XCTAssertEqual(DDRUMMethod.get.swiftType, .get) + XCTAssertEqual(DDRUMMethod.head.swiftType, .head) + XCTAssertEqual(DDRUMMethod.put.swiftType, .put) + XCTAssertEqual(DDRUMMethod.delete.swiftType, .delete) + XCTAssertEqual(DDRUMMethod.patch.swiftType, .patch) + XCTAssertEqual(DDRUMMethod.connect.swiftType, .connect) + XCTAssertEqual(DDRUMMethod.trace.swiftType, .trace) + XCTAssertEqual(DDRUMMethod.options.swiftType, .options) + } +} + +class DDRUMMonitorTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var config: RUM.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + CoreRegistry.register(default: core) + config = RUM.Configuration(applicationID: .mockAny()) + } + + override func tearDown() { + core.flushAndTearDown() + config = nil + CoreRegistry.unregisterDefault() + core = nil + super.tearDown() + } + + func testWhenSwiftRUMIsNotEnabled_thenObjcMonitorIsNotRegistered() { + XCTAssertTrue(DDRUMMonitor.shared().swiftRUMMonitor is NOPMonitor) + } + + func testWhenSwiftRUMIsEnabled_thenObjcMonitorIsRegistered() { + RUM.enable(with: config) + XCTAssertTrue(DDRUMMonitor.shared().swiftRUMMonitor is Monitor) + } + + func testProvidingCurrentSessionID() throws { + let callSessionIDCallback = expectation(description: "call session ID callback") + var currentSessionID: String? = nil + + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + objcRUMMonitor.currentSessionID { sessionID in + currentSessionID = sessionID + callSessionIDCallback.fulfill() + } + + waitForExpectations(timeout: 0.5) + let sessionID = try XCTUnwrap(currentSessionID) + XCTAssertTrue(sessionID.matches(regex: .uuidRegex)) + } + + func testStoppingSession() throws { + let callSessionIDCallback = expectation(description: "call session ID callback twice") + callSessionIDCallback.expectedFulfillmentCount = 2 + var sessionID1: String? = nil + var sessionID2: String? = nil + + // Given + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + objcRUMMonitor.currentSessionID { sessionID in + sessionID1 = sessionID + callSessionIDCallback.fulfill() + } + + // When + objcRUMMonitor.stopSession() + objcRUMMonitor.startView(key: "key", name: "AnyView", attributes: [:]) + + // Then + objcRUMMonitor.currentSessionID { sessionID in + sessionID2 = sessionID + callSessionIDCallback.fulfill() + } + + waitForExpectations(timeout: 0.5) + XCTAssertNotEqual(try XCTUnwrap(sessionID1), try XCTUnwrap(sessionID2)) + } + + func testSendingViewEvents() throws { + RUM.enable(with: config) + + let objcRUMMonitor = DDRUMMonitor.shared() + let mockView = createMockView(viewControllerClassName: "FirstViewController") + + objcRUMMonitor.startView(viewController: mockView, name: "FirstView", attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.stopView(viewController: mockView, attributes: ["event-attribute2": "foo2"]) + objcRUMMonitor.startView(key: "view2", name: "SecondView", attributes: ["event-attribute1": "bar1"]) + objcRUMMonitor.stopView(key: "view2", attributes: ["event-attribute2": "bar2"]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let viewEvents = rumEventMatchers.filterRUMEvents(ofType: RUMViewEvent.self) { event in + return event.view.name != RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName + } + XCTAssertEqual(viewEvents.count, 4) + + let event1: RUMViewEvent = try viewEvents[0].model() + let event2: RUMViewEvent = try viewEvents[1].model() + let event3: RUMViewEvent = try viewEvents[2].model() + let event4: RUMViewEvent = try viewEvents[3].model() + XCTAssertEqual(event1.view.name, "FirstView") + XCTAssertEqual(event1.view.url, "FirstViewController") + XCTAssertEqual(event2.view.name, "FirstView") + XCTAssertEqual(event2.view.url, "FirstViewController") + XCTAssertEqual(event3.view.name, "SecondView") + XCTAssertEqual(event3.view.url, "view2") + XCTAssertEqual(event4.view.name, "SecondView") + XCTAssertEqual(event4.view.url, "view2") + XCTAssertEqual(try viewEvents[1].attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try viewEvents[1].attribute(forKeyPath: "context.event-attribute2"), "foo2") + XCTAssertEqual(try viewEvents[3].attribute(forKeyPath: "context.event-attribute1"), "bar1") + XCTAssertEqual(try viewEvents[3].attribute(forKeyPath: "context.event-attribute2"), "bar2") + } + + func testSendingViewEventsWithTiming() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.startView(viewController: mockView, name: "SomeView", attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.addTiming(name: "timing") + objcRUMMonitor.stopView(viewController: mockView, attributes: ["event-attribute2": "foo2"]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let viewEvents = rumEventMatchers.filterRUMEvents(ofType: RUMViewEvent.self) { event in + return event.view.name != RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName + } + XCTAssertEqual(viewEvents.count, 3) + + let event1: RUMViewEvent = try viewEvents[0].model() + let event2: RUMViewEvent = try viewEvents[1].model() + XCTAssertEqual(event1.view.name, "SomeView") + XCTAssertEqual(event2.view.name, "SomeView") + XCTAssertEqual(try viewEvents.first?.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try viewEvents.last?.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try viewEvents.last?.attribute(forKeyPath: "context.event-attribute2"), "foo2") + XCTAssertNotNil(try? viewEvents.last?.timing(named: "timing")) + } + + func testSendingResourceEvents() throws { + guard #available(iOS 13, *) else { + return // `URLSessionTaskMetrics` mocking doesn't work prior to iOS 13.0 + } + + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.startView(viewController: mockView, name: .mockAny(), attributes: [:]) + + objcRUMMonitor.startResource(resourceKey: "/resource1", url: URL(string: "https://foo.com/1")!, attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.addResourceMetrics( + resourceKey: "/resource1", + metrics: .mockWith( + taskInterval: .init(start: .mockDecember15th2019At10AMUTC(), end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 2)), + transactionMetrics: [ + .mockBySpreadingDetailsBetween(start: .mockDecember15th2019At10AMUTC(), end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 2)) + ] + ), + attributes: ["event-attribute2": "foo2"] + ) + objcRUMMonitor.stopResource(resourceKey: "/resource1", response: .mockAny(), size: nil, attributes: ["event-attribute3": "foo3"]) + + objcRUMMonitor.startResource(resourceKey: "/resource2", httpMethod: .get, urlString: "/some/url/2", attributes: [:]) + objcRUMMonitor.stopResource(resourceKey: "/resource2", statusCode: 333, kind: .beacon, size: 142, attributes: [:]) + + objcRUMMonitor.startResource(resourceKey: "/resource3", httpMethod: .get, urlString: "/some/url/3", attributes: [:]) + objcRUMMonitor.stopResource(resourceKey: "/resource3", response: .mockAny(), size: 242, attributes: [:]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let resourceEvents = rumEventMatchers.filterRUMEvents(ofType: RUMResourceEvent.self) + XCTAssertEqual(resourceEvents.count, 3) + + let event1Matcher = resourceEvents[0] + let event1: RUMResourceEvent = try event1Matcher.model() + XCTAssertEqual(event1.resource.url, "https://foo.com/1") + XCTAssertEqual(event1.resource.duration, 2_000_000_000) + XCTAssertNotNil(event1.resource.dns) + XCTAssertEqual(try event1Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try event1Matcher.attribute(forKeyPath: "context.event-attribute2"), "foo2") + XCTAssertEqual(try event1Matcher.attribute(forKeyPath: "context.event-attribute3"), "foo3") + + let event2Matcher = resourceEvents[1] + let event2: RUMResourceEvent = try event2Matcher.model() + XCTAssertEqual(event2.resource.url, "/some/url/2") + XCTAssertEqual(event2.resource.size, 142) + XCTAssertEqual(event2.resource.type, .beacon) + XCTAssertEqual(event2.resource.statusCode, 333) + + let event3: RUMResourceEvent = try resourceEvents[2].model() + XCTAssertEqual(event3.resource.url, "/some/url/3") + XCTAssertEqual(event3.resource.size, 242) + } + + func testSendingErrorEvents() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.startView(viewController: mockView, name: .mockAny(), attributes: [:]) + + let request: URLRequest = .mockAny() + let error = ErrorMock("error details") + objcRUMMonitor.startResource(resourceKey: "/resource1", request: request, attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.stopResourceWithError( + resourceKey: "/resource1", error: error, response: .mockAny(), attributes: ["event-attribute2": "foo2"] + ) + + objcRUMMonitor.startResource(resourceKey: "/resource2", request: request, attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.stopResourceWithError( + resourceKey: "/resource2", message: "error message", response: .mockAny(), attributes: ["event-attribute2": "foo2"] + ) + + objcRUMMonitor.addError(error: error, source: .custom, attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.addError(message: "error message", stack: "error stack", source: .source, attributes: [:]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let errorEvents = rumEventMatchers.filterRUMEvents(ofType: RUMErrorEvent.self) + XCTAssertEqual(errorEvents.count, 4) + + let event1Matcher = errorEvents[0] + let event1: RUMErrorEvent = try event1Matcher.model() + XCTAssertEqual(event1.error.resource?.url, request.url!.absoluteString) + XCTAssertEqual(event1.error.type, "ErrorMock") + XCTAssertEqual(event1.error.message, "error details") + XCTAssertEqual(event1.error.source, .network) + XCTAssertEqual(event1.error.stack, "error details") + XCTAssertEqual(try event1Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try event1Matcher.attribute(forKeyPath: "context.event-attribute2"), "foo2") + + let event2Matcher = errorEvents[1] + let event2: RUMErrorEvent = try event2Matcher.model() + XCTAssertEqual(event2.error.resource?.url, request.url!.absoluteString) + XCTAssertEqual(event2.error.message, "error message") + XCTAssertEqual(event2.error.source, .network) + XCTAssertNil(event2.error.stack) + XCTAssertEqual(try event2Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try event2Matcher.attribute(forKeyPath: "context.event-attribute2"), "foo2") + + let event3Matcher = errorEvents[2] + let event3: RUMErrorEvent = try event3Matcher.model() + XCTAssertNil(event3.error.resource) + XCTAssertEqual(event3.error.type, "ErrorMock") + XCTAssertEqual(event3.error.message, "error details") + XCTAssertEqual(event3.error.source, .custom) + XCTAssertEqual(event3.error.stack, "error details") + XCTAssertEqual(try event3Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + + let event4Matcher = errorEvents[3] + let event4: RUMErrorEvent = try event4Matcher.model() + XCTAssertEqual(event4.error.message, "error message") + XCTAssertEqual(event4.error.source, .source) + XCTAssertEqual(event4.error.stack, "error stack") + } + + func testSendingActionEvents() throws { + config.dateProvider = RelativeDateProvider(startingFrom: Date(), advancingBySeconds: 1) + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.startView(viewController: mockView, name: .mockAny(), attributes: [:]) + + objcRUMMonitor.addAction(type: .tap, name: "tap action", attributes: ["event-attribute1": "foo1"]) + + objcRUMMonitor.startAction(type: .swipe, name: "swipe action", attributes: ["event-attribute1": "foo1"]) + objcRUMMonitor.stopAction(type: .swipe, name: "swipe action", attributes: ["event-attribute2": "foo2"]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let actionEvents = rumEventMatchers.filterRUMEvents(ofType: RUMActionEvent.self) + XCTAssertEqual(actionEvents.count, 3) + + let event1Matcher = actionEvents[0] + let event1: RUMActionEvent = try event1Matcher.model() + XCTAssertEqual(event1.action.type, .applicationStart) + + let event2Matcher = actionEvents[1] + let event2: RUMActionEvent = try event2Matcher.model() + XCTAssertEqual(event2.action.type, .tap) + XCTAssertEqual(try event2Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + + let event3Matcher = actionEvents[2] + let event3: RUMActionEvent = try event3Matcher.model() + XCTAssertEqual(event3.action.type, .swipe) + XCTAssertEqual(try event3Matcher.attribute(forKeyPath: "context.event-attribute1"), "foo1") + XCTAssertEqual(try event3Matcher.attribute(forKeyPath: "context.event-attribute2"), "foo2") + } + + func testSendingGlobalAttributes() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.addAttribute(forKey: "global-attribute1", value: "foo1") + objcRUMMonitor.addAttribute(forKey: "global-attribute2", value: "foo2") + objcRUMMonitor.removeAttribute(forKey: "global-attribute2") + + objcRUMMonitor.startView(viewController: mockView, name: .mockAny(), attributes: ["event-attribute1": "foo1"]) + + let rumEventMatchers = try core.waitAndReturnRUMEventMatchers() + + let viewEvents = rumEventMatchers.filterRUMEvents(ofType: RUMViewEvent.self) { event in + return event.view.name != RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName + } + XCTAssertEqual(viewEvents.count, 1) + + XCTAssertEqual(try viewEvents[0].attribute(forKeyPath: "context.global-attribute1"), "foo1") + XCTAssertNil(try? viewEvents[0].attribute(forKeyPath: "context.global-attribute2") as String) + XCTAssertEqual(try viewEvents[0].attribute(forKeyPath: "context.event-attribute1"), "foo1") + } + + func testEvaluatingFeatureFlags() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.addFeatureFlagEvaluation(name: "flag1", value: "value1") + objcRUMMonitor.addFeatureFlagEvaluation(name: "flag2", value: true) + + let viewEvents = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self) + let lastView = try XCTUnwrap(viewEvents.last) + XCTAssertEqual(lastView.featureFlags!.featureFlagsInfo["flag1"] as? AnyEncodable, AnyEncodable("value1")) + XCTAssertEqual(lastView.featureFlags!.featureFlagsInfo["flag2"] as? AnyEncodable, AnyEncodable(true)) + } + + func testChangingDebugFlag() throws { + RUM.enable(with: config) + let objcRUMMonitor = DDRUMMonitor.shared() + + objcRUMMonitor.debug = true + XCTAssertTrue(objcRUMMonitor.swiftRUMMonitor.debug) + + objcRUMMonitor.debug = false + XCTAssertFalse(objcRUMMonitor.swiftRUMMonitor.debug) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDRUMTests.swift b/DatadogCore/Tests/DatadogObjc/DDRUMTests.swift new file mode 100644 index 0000000000..2dd28763d2 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDRUMTests.swift @@ -0,0 +1,36 @@ +/* + * 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 +@testable import DatadogRUM +@testable import DatadogObjc + +class DDRUMTests: XCTestCase { + private var core: FeatureRegistrationCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = FeatureRegistrationCoreMock() + CoreRegistry.register(default: core) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() + core = nil + super.tearDown() + } + + func testWhenNotEnabled() { + XCTAssertTrue(DDRUMMonitor.shared().swiftRUMMonitor is NOPMonitor) + } + + func testWhenEnabled() { + DDRUM.enable(with: DDRUMConfiguration(applicationID: "app-id")) + XCTAssertTrue(DDRUMMonitor.shared().swiftRUMMonitor is Monitor) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.swift new file mode 100644 index 0000000000..7e4aff6885 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDTraceConfigurationTests.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 XCTest +import TestUtilities +import DatadogInternal +@testable import DatadogTrace +@testable import DatadogObjc + +class DDTraceConfigurationTests: XCTestCase { + private var objc = DDTraceConfiguration() + private var swift: Trace.Configuration { objc.swiftConfig } + + func testSampleRate() { + objc.sampleRate = 30 + XCTAssertEqual(objc.sampleRate, 30) + XCTAssertEqual(swift.sampleRate, 30) + } + + func testService() { + objc.service = "custom-service" + XCTAssertEqual(objc.service, "custom-service") + XCTAssertEqual(swift.service, "custom-service") + } + + func testTags() { + let random: [String: Any] = mockRandomAttributes() + objc.tags = random + DDAssertJSONEqual(objc.tags!, random) + DDAssertReflectionEqual(swift.tags!, random.dd.swiftAttributes) + } + + func testSetDDTraceURLSessionTracking() { + var tracking: DDTraceURLSessionTracking + + tracking = DDTraceURLSessionTracking(firstPartyHostsTracing: .init(hosts: ["foo.com"])) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .trace(hosts: ["foo.com"]))) + + tracking = DDTraceURLSessionTracking(firstPartyHostsTracing: .init(hosts: ["foo.com"], sampleRate: 99)) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .trace(hosts: ["foo.com"], sampleRate: 99))) + + tracking = DDTraceURLSessionTracking(firstPartyHostsTracing: .init(hostsWithHeaderTypes: ["foo.com": [.b3, .datadog]])) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: ["foo.com": [.b3, .datadog]]))) + + tracking = DDTraceURLSessionTracking(firstPartyHostsTracing: .init(hostsWithHeaderTypes: ["foo.com": [.b3, .datadog]], sampleRate: 99)) + objc.setURLSessionTracking(tracking) + DDAssertReflectionEqual(swift.urlSessionTracking, .init(firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: ["foo.com": [.b3, .datadog]], sampleRate: 99))) + } + + func testBundleWithRUM() { + let random: Bool = .mockRandom() + objc.bundleWithRumEnabled = random + XCTAssertEqual(objc.bundleWithRumEnabled, random) + XCTAssertEqual(swift.bundleWithRumEnabled, random) + } + + func testSendNetworkInfo() { + let random: Bool = .mockRandom() + objc.networkInfoEnabled = random + XCTAssertEqual(objc.networkInfoEnabled, random) + XCTAssertEqual(swift.networkInfoEnabled, random) + } + + func testCustomEndpoint() { + let random: URL = .mockRandom() + objc.customEndpoint = random + XCTAssertEqual(objc.customEndpoint, random) + XCTAssertEqual(swift.customEndpoint, random) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDTraceTests.swift b/DatadogCore/Tests/DatadogObjc/DDTraceTests.swift new file mode 100644 index 0000000000..acdab9f29c --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDTraceTests.swift @@ -0,0 +1,36 @@ +/* + * 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 +@testable import DatadogTrace +@testable import DatadogObjc + +class DDTraceTests: XCTestCase { + private var core: FeatureRegistrationCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = FeatureRegistrationCoreMock() + CoreRegistry.register(default: core) + } + + override func tearDown() { + CoreRegistry.unregisterDefault() + core = nil + super.tearDown() + } + + func testWhenNotEnabled() { + XCTAssertTrue(DDTracer.shared().dd?.swiftTracer is DDNoopTracer) + } + + func testWhenEnabled() { + DDTrace.enable(with: DDTraceConfiguration()) + XCTAssertTrue(DDTracer.shared().dd?.swiftTracer is DatadogTracer) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift new file mode 100644 index 0000000000..7119f7d66b --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift @@ -0,0 +1,401 @@ +/* + * 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 + +@testable import DatadogLogs +@testable import DatadogTrace +@testable import DatadogCore +@testable import DatadogObjc + +class DDTracerTests: XCTestCase { + private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional + private var config: Trace.Configuration! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = DatadogCoreProxy() + CoreRegistry.register(default: core) + config = Trace.Configuration() + } + + override func tearDown() { + core.flushAndTearDown() + config = nil + CoreRegistry.unregisterDefault() + core = nil + super.tearDown() + } + + func testWhenSwiftTraceIsNotEnabled_thenObjcTracerIsNotRegistered() { + XCTAssertTrue(DDTracer.shared().dd?.swiftTracer is DDNoopTracer) + } + + func testWhenSwiftTraceIsEnabled_thenObjcTracerIsRegistered() { + Trace.enable(with: config) + XCTAssertTrue(DDTracer.shared().dd?.swiftTracer is DatadogTracer) + } + + func testSendingCustomizedSpans() throws { + Trace.enable(with: config) + + let objcTracer = DDTracer.shared() + + let objcSpan1 = objcTracer.startSpan("operation") + let objcSpan2 = objcTracer.startSpan( + "operation", + tags: NSDictionary(dictionary: ["tag1": NSString(string: "value1"), "tag2": NSInteger(integerLiteral: 123)]) + ) + let objcSpan3 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context + ) + let objcSpan4 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context, + tags: NSDictionary(dictionary: ["tag1": NSString(string: "value1"), "tag2": NSInteger(integerLiteral: 123)]) + ) + let objcSpan5 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context, + tags: NSDictionary( + dictionary: [ + "tag1": NSString(string: "value1"), + "tag2": NSInteger(integerLiteral: 123), + "nsurlTag": NSURL(string: "https://example.com/image.png")! + ] + ), + startTime: .mockDecember15th2019At10AMUTC() + ) + + objcSpan5.setOperationName("updated operation name") + objcSpan5.setTag("nsstringTag", value: NSString(string: "string value")) + objcSpan5.setTag("nsnumberTag", numberValue: NSNumber(value: 10.5)) + objcSpan5.setTag("nsboolTag", boolValue: true) + + _ = objcSpan5.setBaggageItem("item", value: "value") + XCTAssertEqual(objcSpan5.getBaggageItem("item"), "value") + + var baggageItems: [(key: String, value: String)] = [] + objcSpan5.context.forEachBaggageItem { itemKey, itemValue in + baggageItems.append((key: itemKey, value: itemValue)) + return false + } + XCTAssertEqual(baggageItems.count, 1) + XCTAssertEqual(baggageItems[0].key, "item") + XCTAssertEqual(baggageItems[0].value, "value") + + objcSpan1.finish() + objcSpan2.finish() + objcSpan3.finish() + objcSpan4.finishWithTime(nil) + objcSpan5.finishWithTime(.mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + [objcSpan1, objcSpan2, objcSpan3, objcSpan4, objcSpan5].forEach { span in + XCTAssertTrue(span.tracer === objcTracer) + } + + let spanMatchers = try core.waitAndReturnSpanMatchers() + + // assert operation name + try spanMatchers[0...3].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.operationName(), "operation") + } + XCTAssertEqual(try spanMatchers[4].operationName(), "updated operation name") + + // assert parent-child relationship + try spanMatchers[2...4].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.traceID(), try spanMatchers[0].traceID()) + XCTAssertEqual(try spanMatcher.parentSpanID(), try spanMatchers[0].spanID()) + } + + // assert tags + try [spanMatchers[1], spanMatchers[3], spanMatchers[4]].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag1"), "value1") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag2"), "123") + } + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsurlTag"), "https://example.com/image.png") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsstringTag"), "string value") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsnumberTag"), "10.5") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsboolTag"), "true") + + // assert baggage item + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.item"), "value") + + // assert timing + XCTAssertEqual(try spanMatchers[4].startTime(), 1_576_404_000_000_000_000) + XCTAssertEqual(try spanMatchers[4].duration(), 500_000_000) + } + + func testSendingSpanLogs() throws { + Logs.enable() + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + + let objcSpan = objcTracer.startSpan("operation") + objcSpan.log(["foo": NSString(string: "bar")], timestamp: Date.mockDecember15th2019At10AMUTC()) + objcSpan.log(["bizz": NSNumber(10.5)]) + objcSpan.log(["buzz": NSURL(string: "https://example.com/image.png")!], timestamp: nil) + + let logMatchers = try core.waitAndReturnLogMatchers() + + logMatchers[0].assertValue(forKey: "foo", equals: "bar") + logMatchers[1].assertValue(forKey: "bizz", equals: 10.5) + logMatchers[2].assertValue(forKey: "buzz", equals: "https://example.com/image.png") + objcSpan.finish() + } + + func testSendingSpanLogsWithErrorFromArguments() throws { + Logs.enable() + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + + let objcSpan = objcTracer.startSpan("operation") + objcSpan.log(["foo": NSString(string: "bar")], timestamp: Date.mockDecember15th2019At10AMUTC()) + objcSpan.setError(kind: "Swift error", message: "Ops!", stack: nil) + + let logMatchers = try core.waitAndReturnLogMatchers() + + logMatchers[0].assertValue(forKey: "foo", equals: "bar") + + let errorLogMatcher = logMatchers[1] + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Swift error") + errorLogMatcher.assertMessage(equals: "Ops!") + objcSpan.finish() + } + + func testSendingSpanLogsWithErrorFromNSError() throws { + Logs.enable() + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + + let objcSpan = objcTracer.startSpan("operation") + objcSpan.log(["foo": NSString(string: "bar")], timestamp: Date.mockDecember15th2019At10AMUTC()) + let error = NSError( + domain: "Tracer", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Ops!"] + ) + objcSpan.setError(error) + + let logMatchers = try core.waitAndReturnLogMatchers() + + logMatchers[0].assertValue(forKey: "foo", equals: "bar") + + let errorLogMatcher = logMatchers[1] + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Tracer - 1") + errorLogMatcher.assertMessage(equals: "Ops!") + objcSpan.finish() + } + + func testInjectingSpanContextToValidCarrierAndFormat() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "x-datadog-trace-id": "100", + "x-datadog-parent-id": "200", + "x-datadog-sampling-priority": "1", + "x-datadog-tags": "_dd.p.tid=a" + ] + XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + } + + func testInjectingRejectedSpanContextToValidCarrierAndFormat() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .sampled + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + XCTAssertEqual(objcWriter.traceHeaderFields, [:]) + } + + func testInjectingSpanContextToInvalidCarrierOrFormat() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) + + let objcValidWriter = DDHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + let objcInvalidFormat = "foo" + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) + ) + + let objcInvalidWriter = NSObject() + let objcValidFormat = OT.formatTextMap + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcValidFormat, carrier: objcInvalidWriter) + ) + } + + func testInjectingSpanContextToValidCarrierAndFormatForB3() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "b3": "000000000000000a0000000000000064-00000000000000c8-1-0000000000000000" + ] + XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + } + + func testInjectingRejectedSpanContextToValidCarrierAndFormatForB3() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .all + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "b3": "0", + ] + XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + } + + func testInjectingSpanContextToInvalidCarrierOrFormatForB3() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) + + let objcValidWriter = DDB3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + let objcInvalidFormat = "foo" + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) + ) + + let objcInvalidWriter = NSObject() + let objcValidFormat = OT.formatTextMap + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcValidFormat, carrier: objcInvalidWriter) + ) + } + + func testInjectingSpanContextToValidCarrierAndFormatForW3C() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "traceparent": "00-000000000000000a0000000000000064-00000000000000c8-01", + "tracestate": "dd=p:00000000000000c8;s:1" + ] + XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + } + + func testInjectingRejectedSpanContextToValidCarrierAndFormatForW3C() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) + ) + + let objcWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + traceContextInjection: .all + ) + try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "traceparent": "00-000000000000000a0000000000000064-00000000000000c8-00", + "tracestate": "dd=p:00000000000000c8;s:0" + ] + XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) + } + + func testInjectingSpanContextToInvalidCarrierOrFormatForW3C() throws { + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) + + let objcValidWriter = DDW3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + traceContextInjection: .all + ) + let objcInvalidFormat = "foo" + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) + ) + + let objcInvalidWriter = NSObject() + let objcValidFormat = OT.formatTextMap + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcValidFormat, carrier: objcInvalidWriter) + ) + } + + // MARK: - Usage errors + + func testsWhenTagsDictionaryContainsInvalidKeys_thenThosesTagsAreDropped() throws { + // Given + Trace.enable(with: config) + let objcTracer = DDTracer.shared() + + // When + let tags = NSDictionary( + dictionary: [ + 123: "tag with invalid key", + "valid-tag": "tag with valid key" + ] + ) + let objcSpan = objcTracer.startSpan(.mockAny(), tags: tags) + objcSpan.finish() + + // Then + let spanMatchers = try core.waitAndReturnSpanMatchers() + XCTAssertEqual(spanMatchers.count, 1) + XCTAssertNil(try? spanMatchers[0].meta.custom(keyPath: "meta.123"), "123 is not a valid tag-key, so it should be dropped") + XCTAssertNotNil(try? spanMatchers[0].meta.custom(keyPath: "meta.valid-tag")) + } +} diff --git a/DatadogCore/Tests/DatadogObjc/DDUIKitRUMActionsPredicateTests.swift b/DatadogCore/Tests/DatadogObjc/DDUIKitRUMActionsPredicateTests.swift new file mode 100644 index 0000000000..3bcfe8d41d --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDUIKitRUMActionsPredicateTests.swift @@ -0,0 +1,69 @@ +/* + * 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 DatadogObjc +import DatadogRUM + +#if canImport(SwiftUI) +import SwiftUI +#endif + +class DDUIKitRUMActionsPredicateTests: XCTestCase { + func testGivenDefaultPredicate_whenAskingForCustomView_itNamesTheActionByItsClassName() { + // Given + let predicate = DDDefaultUIKitRUMActionsPredicate() + + // When + #if os(tvOS) + let rumAction = predicate.rumAction(press: .select, targetView: UIButton()) + #else + let rumAction = predicate.rumAction(targetView: UIButton()) + #endif + // Then + XCTAssertEqual(rumAction?.name, "UIButton") + XCTAssertTrue(rumAction!.attributes.isEmpty) + } + + func testGivenDefaultPredicate_whenAskingForViewWithAccesiblityIdentifier_itNamesTheActionWithIt() { + // Given + let predicate = DDDefaultUIKitRUMActionsPredicate() + let targetView = UIButton() + targetView.accessibilityIdentifier = "Identifier" + + // When + #if os(tvOS) + let rumAction = predicate.rumAction(press: .select, targetView: targetView) + #else + let rumAction = predicate.rumAction(targetView: targetView) + #endif + + // Then + XCTAssertEqual(rumAction?.name, "UIButton(Identifier)") + XCTAssertTrue(rumAction!.attributes.isEmpty) + } + +#if canImport(SwiftUI) + func testGivenDefaultPredicate_whenAskingSwiftUIView_itReturnsAction() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + // Given + let predicate = DDDefaultUIKitRUMActionsPredicate() + + // When + let swiftUIView = UIHostingController(rootView: EmptyView()).view! + #if os(tvOS) + let rumAction = predicate.rumAction(press: .select, targetView: swiftUIView) + #else + let rumAction = predicate.rumAction(targetView: swiftUIView) + #endif + + // Then + XCTAssertNotNil(rumAction) + } +#endif +} diff --git a/DatadogCore/Tests/DatadogObjc/DDUIKitRUMViewsPredicateTests.swift b/DatadogCore/Tests/DatadogObjc/DDUIKitRUMViewsPredicateTests.swift new file mode 100644 index 0000000000..bcfcd99a8a --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDUIKitRUMViewsPredicateTests.swift @@ -0,0 +1,71 @@ +/* + * 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 DatadogObjc + +@testable import DatadogRUM + +#if canImport(SwiftUI) +import SwiftUI +#endif + +class DDUIKitRUMViewsPredicateTests: XCTestCase { + func testGivenDefaultPredicate_whenAskingForCustomSwiftViewController_itNamesTheViewByItsClassName() { + // Given + let predicate = DDDefaultUIKitRUMViewsPredicate() + + // When + let customViewController = createMockView(viewControllerClassName: "CustomSwiftViewController") + let rumView = predicate.rumView(for: customViewController) + + // Then + XCTAssertEqual(rumView?.name, "CustomSwiftViewController") + XCTAssertTrue(rumView!.attributes.isEmpty) + } + + func testGivenDefaultPredicate_whenAskingForCustomObjcViewController_itNamesTheViewByItsClassName() { + // Given + let predicate = DDDefaultUIKitRUMViewsPredicate() + + // When + let customViewController = CustomObjcViewController() + let rumView = predicate.rumView(for: customViewController) + + // Then + XCTAssertEqual(rumView?.name, "CustomObjcViewController") + XCTAssertTrue(rumView!.attributes.isEmpty) + } + + func testGivenDefaultPredicate_whenAskingUIKitViewController_itReturnsNoView() { + // Given + let predicate = DDDefaultUIKitRUMViewsPredicate() + + // When + let uiKitViewController = UIViewController() + let rumView = predicate.rumView(for: uiKitViewController) + + // Then + XCTAssertNil(rumView) + } + +#if canImport(SwiftUI) + func testGivenDefaultPredicate_whenAskingSwiftUIViewController_itReturnsNoView() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + // Given + let predicate = DDDefaultUIKitRUMViewsPredicate() + + // When + let swiftUIHostingController = UIHostingController(rootView: EmptyView()) + let rumView = predicate.rumView(for: swiftUIHostingController) + + // Then + XCTAssertNil(rumView) + } +#endif +} diff --git a/DatadogCore/Tests/DatadogObjc/DDURLSessionInstrumentationConfigurationTests.swift b/DatadogCore/Tests/DatadogObjc/DDURLSessionInstrumentationConfigurationTests.swift new file mode 100644 index 0000000000..73879a320c --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/DDURLSessionInstrumentationConfigurationTests.swift @@ -0,0 +1,30 @@ +/* + * 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 +@testable import DatadogObjc + +final class DDURLSessionInstrumentationConfigurationTests: XCTestCase { + private var objc = DDURLSessionInstrumentationConfiguration(delegateClass: MockDelegate.self) + private var swift: URLSessionInstrumentation.Configuration { objc.swiftConfig } + + func testDelegateClass() { + XCTAssertTrue(objc.delegateClass === MockDelegate.self) + } + + func testFirstPartyHostsTracing() { + objc.setFirstPartyHostsTracing(.init(hosts: ["example.com", "example.org"])) + DDAssertReflectionEqual(swift.firstPartyHostsTracing, .trace(hosts: ["example.com", "example.org"])) + + objc.setFirstPartyHostsTracing(.init(hostsWithHeaderTypes: ["example.com": [.b3, .datadog]])) + DDAssertReflectionEqual(swift.firstPartyHostsTracing, .traceWithHeaders(hostsWithHeaders: ["example.com": [.b3, .datadog]])) + } + + class MockDelegate: NSObject, URLSessionDataDelegate { + } +} diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m new file mode 100644 index 0000000000..35094c851d --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m @@ -0,0 +1,32 @@ +/* +* 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 DDB3HTTPHeadersWriter_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDB3HTTPHeadersWriter_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testInitWithSamplingRate { + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] + injectEncoding:DDInjectEncodingSingle + traceContextInjection:DDTraceContextInjectionAll]; + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] + injectEncoding:DDInjectEncodingMultiple + traceContextInjection:DDTraceContextInjectionAll]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m new file mode 100644 index 0000000000..f4f766163c --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDConfiguration+apiTests.m @@ -0,0 +1,77 @@ +/* +* 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; +@import DatadogCrashReporting; + +// MARK: - DDDataEncryption + +@interface CustomDDDataEncryption: NSObject +@end + +@implementation CustomDDDataEncryption + +- (NSData * _Nullable)decryptWithData:(NSData * _Nonnull)data error:(NSError * _Nullable __autoreleasing * _Nullable)error { + return data; +} + +- (NSData * _Nullable)encryptWithData:(NSData * _Nonnull)data error:(NSError * _Nullable __autoreleasing * _Nullable)error { + return data; +} + +@end + +// MARK: - Tests + +@interface DDConfiguration_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDConfiguration_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDSiteAPI { + [DDSite eu1]; + [DDSite us1]; + [DDSite us1]; + [DDSite us1_fed]; + [DDSite us3]; + [DDSite us5]; +} + +- (void)testDDBatchSizeAPI { + DDBatchSizeSmall; DDBatchSizeMedium; DDBatchSizeLarge; +} + +- (void)testDDUploadFrequencyAPI { + DDUploadFrequencyRare; DDUploadFrequencyAverage; DDUploadFrequencyFrequent; +} + +- (void)testDDConfigurationBuilderAPI { + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + + configuration.site = [DDSite us1]; + configuration.service = @""; + configuration.bundle = [NSBundle mainBundle]; + configuration.batchSize = DDBatchSizeMedium; + configuration.uploadFrequency = DDUploadFrequencyAverage; + configuration.additionalConfiguration = @{@"additional": @"config"}; + [configuration setEncryption:[CustomDDDataEncryption new]]; + configuration.backgroundTasksEnabled = true; +} + +- (void)testDatadogCrashReporterAPI { + [DDCrashReporter enable]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m new file mode 100644 index 0000000000..1fd8d2c746 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDDatadog+apiTests.m @@ -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 +@import DatadogObjc; + +@interface DDDatadog_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDDatadog_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDTrackingConsentAPI { + [DDTrackingConsent granted]; + [DDTrackingConsent notGranted]; + [DDTrackingConsent pending]; +} + +- (void)testDDDatadog { + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + + [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + + [DDDatadog isInitialized]; + + DDSDKVerbosityLevel verbosity = [DDDatadog verbosityLevel]; + [DDDatadog setVerbosityLevel:verbosity]; + + [DDDatadog setUserInfoWithId:@"" name:@"" email:@"" extraInfo:@{}]; + [DDDatadog addUserExtraInfo:@{}]; + [DDDatadog setTrackingConsentWithConsent:[DDTrackingConsent notGranted]]; + + [DDDatadog clearAllData]; + [DDDatadog stopInstance]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m new file mode 100644 index 0000000000..1b03444744 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m @@ -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 +@import DatadogObjc; + +@interface DDHTTPHeadersWriter_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDHTTPHeadersWriter_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testInitWithSamplingRate { + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] traceContextInjection:DDTraceContextInjectionAll]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] traceContextInjection:DDTraceContextInjectionAll]; +} + +#pragma clang diagnostic pop + +@end 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/DDNSURLSessionDelegate+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDNSURLSessionDelegate+apiTests.m new file mode 100644 index 0000000000..61c93c23cf --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDNSURLSessionDelegate+apiTests.m @@ -0,0 +1,53 @@ +/* +* 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; +@import DatadogTrace; + +@interface DDNSURLSessionDelegate_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDNSURLSessionDelegate_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)setUp { + [super setUp]; + + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + DDTraceFirstPartyHostsTracing *tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + DDTraceURLSessionTracking *urlSessionTracking = [[DDTraceURLSessionTracking alloc] initWithFirstPartyHostsTracing:tracing]; + [config setURLSessionTracking:urlSessionTracking]; + [DDTrace enableWith:config]; +} + +- (void)tearDown { + [super tearDown]; + + [DDURLSessionInstrumentation disableWithDelegateClass:[DDNSURLSessionDelegate class]]; + [DDDatadog clearAllData]; + [DDDatadog flushAndDeinitialize]; +} + +- (void)testDDNSURLSessionDelegateAPI { + [[DDNSURLSessionDelegate alloc] init]; + [[DDNSURLSessionDelegate alloc] initWithAdditionalFirstPartyHosts:[NSSet setWithArray:@[]]]; + [[DDNSURLSessionDelegate alloc] initWithAdditionalFirstPartyHostsWithHeaderTypes:@{ + @"host": [[NSSet alloc] initWithObjects:[DDTracingHeaderType datadog], nil] + }]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m new file mode 100644 index 0000000000..fc9a00fa33 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUM+apiTests.m @@ -0,0 +1,141 @@ +/* +* 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; + +// MARK: - DDUIKitRUMViewsPredicate + +@interface CustomDDUIKitRUMViewsPredicate: NSObject +@end + +@interface CustomDDUIKitRUMViewsPredicate () +@end + +@implementation CustomDDUIKitRUMViewsPredicate +- (DDRUMView * _Nullable)rumViewFor:(UIViewController * _Nonnull)viewController { return nil; } +@end + +// MARK: - DDUIKitRUMActionsPredicate + +@interface CustomDDUIKitRUMActionsPredicate: NSObject +@end + +@interface CustomDDUIKitRUMActionsPredicate () +@end + +@implementation CustomDDUIKitRUMActionsPredicate +- (DDRUMAction * _Nullable)rumActionWithTargetView:(UIView * _Nonnull)targetView { return nil; } +- (DDRUMAction * _Nullable)rumActionWithPress:(enum UIPressType)type targetView:(UIView * _Nonnull)targetView { return nil; } + +@end + +// MARK: - DDRUM tests + +@interface DDRUM_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - minimal assertions, mainly check if the interface is available to Objc. + */ +@implementation DDRUM_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDRUMAPI { + DDRUMConfiguration *config = [[DDRUMConfiguration alloc] initWithApplicationID:@"app-id"]; + [DDRUM enableWith:config]; +} + +- (void)testDDRUMConfigurationAPI { + DDRUMConfiguration *config = [[DDRUMConfiguration alloc] initWithApplicationID:@"app-id"]; + XCTAssertEqual(config.applicationID, @"app-id"); + + XCTAssertEqual(config.sessionSampleRate, 100); + config.sessionSampleRate = 10; + XCTAssertEqual(config.sessionSampleRate, 10); + + XCTAssertEqual(config.telemetrySampleRate, 20); + config.telemetrySampleRate = 30; + XCTAssertEqual(config.telemetrySampleRate, 30); + + XCTAssertNil(config.uiKitViewsPredicate); + CustomDDUIKitRUMViewsPredicate *viewsPredicate = [CustomDDUIKitRUMViewsPredicate new]; + config.uiKitViewsPredicate = viewsPredicate; + XCTAssertIdentical(config.uiKitViewsPredicate, viewsPredicate); + + XCTAssertNil(config.uiKitActionsPredicate); + CustomDDUIKitRUMActionsPredicate *actionsPredicate = [CustomDDUIKitRUMActionsPredicate new]; + config.uiKitActionsPredicate = actionsPredicate; + XCTAssertIdentical(config.uiKitActionsPredicate, actionsPredicate); + + DDRUMURLSessionTracking *urlSessionTracking = [DDRUMURLSessionTracking new]; + DDRUMFirstPartyHostsTracing *tracing; + tracing = [[DDRUMFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + tracing = [[DDRUMFirstPartyHostsTracing alloc] initWithHosts:[NSSet new]]; + tracing = [[DDRUMFirstPartyHostsTracing alloc] initWithHostsWithHeaderTypes:@{}]; + tracing = [[DDRUMFirstPartyHostsTracing alloc] initWithHostsWithHeaderTypes:@{} sampleRate:20]; + [urlSessionTracking setFirstPartyHostsTracing:tracing]; + [urlSessionTracking setResourceAttributesProvider:^NSDictionary * _Nullable(NSURLRequest * _Nonnull request, + NSURLResponse * _Nullable response, + NSData * _Nullable data, + NSError * _Nullable error) { + return @{}; + }]; + + XCTAssertTrue(config.trackFrustrations); + config.trackFrustrations = NO; + XCTAssertFalse(config.trackFrustrations); + + XCTAssertFalse(config.trackBackgroundEvents); + config.trackBackgroundEvents = YES; + XCTAssertTrue(config.trackBackgroundEvents); + + XCTAssertEqual(config.longTaskThreshold, 0.1); + config.longTaskThreshold = 1; + XCTAssertEqual(config.longTaskThreshold, 1); + + XCTAssertEqual(config.appHangThreshold, 0); + config.appHangThreshold = 1; + XCTAssertEqual(config.appHangThreshold, 1); + + XCTAssertEqual(config.vitalsUpdateFrequency, DDRUMVitalsFrequencyAverage); + config.vitalsUpdateFrequency = DDRUMVitalsFrequencyFrequent; + XCTAssertEqual(config.vitalsUpdateFrequency, DDRUMVitalsFrequencyFrequent); + config.vitalsUpdateFrequency = DDRUMVitalsFrequencyNever; + XCTAssertEqual(config.vitalsUpdateFrequency, DDRUMVitalsFrequencyNever); + + [config setViewEventMapper:^DDRUMViewEvent * _Nonnull(DDRUMViewEvent * _Nonnull viewEvent) { + viewEvent.view.url = @""; + return viewEvent; + }]; + [config setResourceEventMapper:^DDRUMResourceEvent * _Nullable(DDRUMResourceEvent * _Nonnull resourceEvent) { + resourceEvent.resource.url = @""; + return resourceEvent; + }]; + [config setActionEventMapper:^DDRUMActionEvent * _Nullable(DDRUMActionEvent * _Nonnull actionEvent) { + return nil; + }]; + [config setErrorEventMapper:^DDRUMErrorEvent * _Nullable(DDRUMErrorEvent * _Nonnull errorEvent) { + return nil; + }]; + [config setLongTaskEventMapper:^DDRUMLongTaskEvent * _Nullable(DDRUMLongTaskEvent * _Nonnull longTaskEvent) { + return nil; + }]; + + XCTAssertNil(config.onSessionStart); + config.onSessionStart = ^(NSString * _Nonnull uuid, BOOL discarded) {}; + XCTAssertNotNil(config.onSessionStart); + + XCTAssertNil(config.customEndpoint); + config.customEndpoint = [NSURL new]; + XCTAssertNotNil(config.customEndpoint); +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m new file mode 100644 index 0000000000..881f9781ba --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDRUMMonitor+apiTests.m @@ -0,0 +1,87 @@ +/* +* 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 DDRUMMonitor_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDRUMMonitor_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDRUMViewAPI { + DDRUMView *view = [[DDRUMView alloc] initWithName:@"abc" attributes:@{@"foo": @"bar"}]; + XCTAssertEqual(view.name, @"abc"); + XCTAssertNotNil(view.attributes[@"foo"]); // TODO: RUMM-1583 assert with `XCTAssertEqual` +} + +- (void)testDDRUMActionAPI { + DDRUMAction *action = [[DDRUMAction alloc] initWithName:@"abc" attributes:@{@"foo": @"bar"}]; + XCTAssertEqual(action.name, @"abc"); + XCTAssertNotNil(action.attributes[@"foo"]); // TODO: RUMM-1583 assert with `XCTAssertEqual` +} + +- (void)testDDRUMErrorSourceAPI { + DDRUMErrorSourceSource; DDRUMErrorSourceNetwork; DDRUMErrorSourceWebview; DDRUMErrorSourceConsole; DDRUMErrorSourceCustom; +} + +- (void)testDDRUMActionTypeAPI { + DDRUMActionTypeTap; DDRUMActionTypeScroll; DDRUMActionTypeSwipe; DDRUMActionTypeCustom; +} + +- (void)testDDRUMResourceTypeAPI { + DDRUMResourceTypeImage; DDRUMResourceTypeXhr; DDRUMResourceTypeBeacon; DDRUMResourceTypeCss; DDRUMResourceTypeDocument; + DDRUMResourceTypeFetch; DDRUMResourceTypeFont; DDRUMResourceTypeJs; DDRUMResourceTypeMedia; DDRUMResourceTypeOther; + DDRUMResourceTypeNative; +} + +- (void)testDDRUMMethodAPI { + DDRUMMethodPost; DDRUMMethodGet; DDRUMMethodHead; DDRUMMethodPut; DDRUMMethodDelete; DDRUMMethodPatch; DDRUMMethodConnect; + DDRUMMethodTrace; DDRUMMethodOptions; +} + +- (void)testDDRUMMonitorAPI { + UIViewController *anyVC = [UIViewController new]; + + DDRUMMonitor *monitor = [DDRUMMonitor shared]; + [monitor currentSessionIDWithCompletion:^(NSString * _Nullable sessionID) {}]; + [monitor stopSession]; + + [monitor startViewWithViewController:anyVC name:@"" attributes:@{}]; + [monitor stopViewWithViewController:anyVC attributes:@{}]; + [monitor startViewWithKey:@"" name:nil attributes:@{}]; + [monitor stopViewWithKey:@"" attributes:@{}]; + [monitor addErrorWithMessage:@"" stack:nil source:DDRUMErrorSourceCustom attributes:@{}]; + [monitor addErrorWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:-100 userInfo:nil] + source:DDRUMErrorSourceNetwork attributes:@{}]; + [monitor startResourceWithResourceKey:@"" request:[NSURLRequest new] attributes:@{}]; + [monitor startResourceWithResourceKey:@"" url:[NSURL new] attributes:@{}]; + [monitor startResourceWithResourceKey:@"" httpMethod:DDRUMMethodGet urlString:@"" attributes:@{}]; + [monitor addResourceMetricsWithResourceKey:@"" metrics:[NSURLSessionTaskMetrics new] attributes:@{}]; + [monitor stopResourceWithResourceKey:@"" response:[NSURLResponse new] size:nil attributes:@{}]; + [monitor stopResourceWithResourceKey:@"" statusCode:nil kind:DDRUMResourceTypeOther size:nil attributes:@{}]; + [monitor stopResourceWithErrorWithResourceKey:@"" + error:[NSError errorWithDomain:NSURLErrorDomain code:-99 userInfo:nil] response:nil attributes:@{}]; + [monitor stopResourceWithErrorWithResourceKey:@"" message:@"" response:nil attributes:@{}]; + [monitor startActionWithType:DDRUMActionTypeSwipe name:@"" attributes:@{}]; + [monitor stopActionWithType:DDRUMActionTypeSwipe name:nil attributes:@{}]; + [monitor addActionWithType:DDRUMActionTypeTap name:@"" attributes:@{}]; + [monitor addAttributeForKey:@"" value:@""]; + [monitor addFeatureFlagEvaluationWithName: @"name" value: @"value"]; + + [monitor setDebug:YES]; + [monitor setDebug:NO]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m new file mode 100644 index 0000000000..bdbe99fba4 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDSessionReplay+apiTests.m @@ -0,0 +1,96 @@ +/* + * 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 DatadogSessionReplay; + +@interface DDSessionReplay_apiTests : XCTestCase +@end + +@implementation DDSessionReplay_apiTests + +// 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/ObjcAPITests/DDTrace+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDTrace+apiTests.m new file mode 100644 index 0000000000..33a1e83345 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDTrace+apiTests.m @@ -0,0 +1,71 @@ +/* +* 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 DDTrace_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - minimal assertions, mainly check if the interface is available to Objc. + */ +@implementation DDTrace_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" +#pragma clang diagnostic ignored "-Wunused-variable" + +- (void)testDDTraceAPI { + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + [DDTrace enableWith:config]; +} + +- (void)testDDTraceConfigurationAPI { + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + + XCTAssertEqual(config.sampleRate, 100); + config.sampleRate = 10; + XCTAssertEqual(config.sampleRate, 10); + + XCTAssertNil(config.service); + config.service = @"custom-service"; + XCTAssertNotNil(config.service); + + XCTAssertNil(config.tags); + config.tags = @{}; + XCTAssertNotNil(config.tags); + + DDTraceFirstPartyHostsTracing *tracing; + tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new]]; + tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHostsWithHeaderTypes:@{}]; + tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHostsWithHeaderTypes:@{} sampleRate:20]; + DDTraceURLSessionTracking *urlSessionTracking = [[DDTraceURLSessionTracking alloc] initWithFirstPartyHostsTracing:tracing]; + + config.bundleWithRumEnabled = NO; + XCTAssertFalse(config.bundleWithRumEnabled); + + config.networkInfoEnabled = YES; + XCTAssertTrue(config.networkInfoEnabled); + + XCTAssertNil(config.customEndpoint); + config.customEndpoint = [NSURL new]; + XCTAssertNotNil(config.customEndpoint); +} + +- (void)testDDTracerAPI { + [[DDTracer shared] startSpan:@""]; + [[DDTracer shared] startSpan:@"" tags:@{}]; + [[DDTracer shared] startSpan:@"" childOf:NULL]; + id span = [[DDTracer shared] startSpan:@"" childOf:NULL tags:NULL startTime:NULL]; + [span finish]; + [span finishWithTime:NULL]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m new file mode 100644 index 0000000000..84f909d021 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDURLSessionInstrumentationTests+apiTests.m @@ -0,0 +1,68 @@ +/* +* 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 +#include +@import DatadogObjc; +@import DatadogTrace; + +#import + +@interface MockDelegate : NSObject +@end + +@implementation MockDelegate +@end + +@interface DDURLSessionInstrumentationTests_apiTests : XCTestCase +@end + +@implementation DDURLSessionInstrumentationTests_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)setUp { + [super setUp]; + + DDConfiguration *configuration = [[DDConfiguration alloc] initWithClientToken:@"abc" env:@"def"]; + [DDDatadog initializeWithConfiguration:configuration trackingConsent:[DDTrackingConsent notGranted]]; + + DDTraceConfiguration *config = [[DDTraceConfiguration alloc] init]; + DDTraceFirstPartyHostsTracing *tracing = [[DDTraceFirstPartyHostsTracing alloc] initWithHosts:[NSSet new] sampleRate:20]; + DDTraceURLSessionTracking *urlSessionTracking = [[DDTraceURLSessionTracking alloc] initWithFirstPartyHostsTracing:tracing]; + [config setURLSessionTracking:urlSessionTracking]; + [DDTrace enableWith:config]; +} + +- (void)tearDown { + [super tearDown]; + + [DDDatadog clearAllData]; + [DDDatadog flushAndDeinitialize]; +} + +- (void)testWorkflow { + XCTestExpectation *expectation = [self expectationWithDescription:@"task completed"]; + DDURLSessionInstrumentationConfiguration *config = [[DDURLSessionInstrumentationConfiguration alloc] initWithDelegateClass:[MockDelegate class]]; + [DDURLSessionInstrumentation enableWithConfiguration:config]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:[MockDelegate new] delegateQueue:nil]; + NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://status.datadoghq.com"] + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + [expectation fulfill]; + }]; + [task resume]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + [DDURLSessionInstrumentation disableWithDelegateClass:[MockDelegate class]]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m new file mode 100644 index 0000000000..ddaaa620f2 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m @@ -0,0 +1,29 @@ +/* +* 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 DDW3CHTTPHeadersWriter_apiTests : XCTestCase +@end + +/* + * `DatadogObjc` APIs smoke tests - only check if the interface is available to Objc. + */ +@implementation DDW3CHTTPHeadersWriter_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testInitWithSamplingRate { + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] traceContextInjection:DDTraceContextInjectionAll]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] traceContextInjection:DDTraceContextInjectionAll]; + +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m new file mode 100644 index 0000000000..d6f908d869 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDWebViewTracking+apiTests.m @@ -0,0 +1,41 @@ +/* +* 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 DatadogWebViewTracking; +@import WebKit; + +@interface WebViewMock: WKWebView +@end + +@implementation WebViewMock +@end + +// MARK: - DDWebViewTracking tests + +@interface DDWebViewTracking_apiTests : XCTestCase +@end + +/* + * `WebViewTracking` APIs smoke tests - minimal assertions, mainly check if the interface is available to Objc. + */ +@implementation DDWebViewTracking_apiTests + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + +- (void)testDDWebViewTrackingAPI { + WebViewMock *webView = [WebViewMock new]; + [DDWebViewTracking enableWithWebView:webView + hosts:[NSSet setWithArray:@[@"host1.com", @"host2.com"]] + logsSampleRate:100.0 + ]; + [DDWebViewTracking disableWithWebView:webView]; +} + +#pragma clang diagnostic pop + +@end diff --git a/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift b/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift new file mode 100644 index 0000000000..b87370e5f3 --- /dev/null +++ b/DatadogCore/Tests/DatadogObjc/RUM/RUMDataModels+objcTests.swift @@ -0,0 +1,113 @@ +/* + * 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 DatadogRUM +@testable import DatadogCore +@testable import DatadogObjc + +class RUMDataModels_objcTests: XCTestCase { + func testGivenObjectiveCViewEventWithAnyAttributes_whenReadingAttributes_theirTypeIsNotAltered() throws { + let expectedContextAttributes: [String: Any] = mockRandomAttributes() + let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes() + + // Given + var swiftView: RUMViewEvent = .mockRandom() + swiftView.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftView.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes + + let objcView = DDRUMViewEvent(swiftModel: swiftView) + + // When + let receivedContextAttributes = try XCTUnwrap(objcView.context?.contextInfo) + let receivedUserInfoAttributes = try XCTUnwrap(objcView.usr?.usrInfo) + + // Then + DDAssertDictionariesEqual(receivedContextAttributes, expectedContextAttributes) + DDAssertDictionariesEqual(receivedUserInfoAttributes, expectedUserInfoAttributes) + } + + func testGivenObjectiveCResourceEventWithAnyAttributes_whenReadingAttributes_theirTypeIsNotAltered() throws { + let expectedContextAttributes: [String: Any] = mockRandomAttributes() + let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes() + + // Given + var swiftResource: RUMResourceEvent = .mockRandom() + swiftResource.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftResource.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes + + let objcResource = DDRUMResourceEvent(swiftModel: swiftResource) + + // When + let receivedContextAttributes = try XCTUnwrap(objcResource.context?.contextInfo) + let receivedUserInfoAttributes = try XCTUnwrap(objcResource.usr?.usrInfo) + + // Then + DDAssertDictionariesEqual(receivedContextAttributes, expectedContextAttributes) + DDAssertDictionariesEqual(receivedUserInfoAttributes, expectedUserInfoAttributes) + } + + func testGivenObjectiveCActionEventWithAnyAttributes_whenReadingAttributes_theirTypeIsNotAltered() throws { + let expectedContextAttributes: [String: Any] = mockRandomAttributes() + let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes() + + // Given + var swiftAction: RUMActionEvent = .mockRandom() + swiftAction.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftAction.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes + + let objcAction = DDRUMActionEvent(swiftModel: swiftAction) + + // When + let receivedContextAttributes = try XCTUnwrap(objcAction.context?.contextInfo) + let receivedUserInfoAttributes = try XCTUnwrap(objcAction.usr?.usrInfo) + + // Then + DDAssertDictionariesEqual(receivedContextAttributes, expectedContextAttributes) + DDAssertDictionariesEqual(receivedUserInfoAttributes, expectedUserInfoAttributes) + } + + func testGivenObjectiveCErrorEventWithAnyAttributes_whenReadingAttributes_theirTypeIsNotAltered() throws { + let expectedContextAttributes: [String: Any] = mockRandomAttributes() + let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes() + + // Given + var swiftError: RUMErrorEvent = .mockRandom() + swiftError.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftError.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes + + let objcError = DDRUMErrorEvent(swiftModel: swiftError) + + // When + let receivedContextAttributes = try XCTUnwrap(objcError.context?.contextInfo) + let receivedUserInfoAttributes = try XCTUnwrap(objcError.usr?.usrInfo) + + // Then + DDAssertDictionariesEqual(receivedContextAttributes, expectedContextAttributes) + DDAssertDictionariesEqual(receivedUserInfoAttributes, expectedUserInfoAttributes) + } + + func testGivenObjectiveCLongTaskEventWithAnyAttributes_whenReadingAttributes_theirTypeIsNotAltered() throws { + let expectedContextAttributes: [String: Any] = mockRandomAttributes() + let expectedUserInfoAttributes: [String: Any] = mockRandomAttributes() + + // Given + var swiftLongTask: RUMLongTaskEvent = .mockRandom() + swiftLongTask.context?.contextInfo = expectedContextAttributes.dd.swiftAttributes + swiftLongTask.usr?.usrInfo = expectedUserInfoAttributes.dd.swiftAttributes + + let objcLongTask = DDRUMLongTaskEvent(swiftModel: swiftLongTask) + + // When + let receivedContextAttributes = try XCTUnwrap(objcLongTask.context?.contextInfo) + let receivedUserInfoAttributes = try XCTUnwrap(objcLongTask.usr?.usrInfo) + + // Then + DDAssertDictionariesEqual(receivedContextAttributes, expectedContextAttributes) + DDAssertDictionariesEqual(receivedUserInfoAttributes, expectedUserInfoAttributes) + } +} diff --git a/Tests/DatadogTests/DatadogPrivate/ObjcExceptionHandlerTests.swift b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift similarity index 75% rename from Tests/DatadogTests/DatadogPrivate/ObjcExceptionHandlerTests.swift rename to DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift index dcbb1cc88b..b74454275c 100644 --- a/Tests/DatadogTests/DatadogPrivate/ObjcExceptionHandlerTests.swift +++ b/DatadogCore/Tests/DatadogPrivate/ObjcExceptionHandlerTests.swift @@ -1,18 +1,16 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import XCTest -import _Datadog_Private +import DatadogCore class ObjcExceptionHandlerTests: XCTestCase { - private let exceptionHandler = 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/Helpers/CustomObjcViewController.h b/DatadogCore/Tests/Helpers/CustomObjcViewController.h new file mode 100644 index 0000000000..79956e42c8 --- /dev/null +++ b/DatadogCore/Tests/Helpers/CustomObjcViewController.h @@ -0,0 +1,15 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface CustomObjcViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/DatadogCore/Tests/Helpers/CustomObjcViewController.m b/DatadogCore/Tests/Helpers/CustomObjcViewController.m new file mode 100644 index 0000000000..49e1dc7740 --- /dev/null +++ b/DatadogCore/Tests/Helpers/CustomObjcViewController.m @@ -0,0 +1,19 @@ +/* + * 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 "CustomObjcViewController.h" + +@interface CustomObjcViewController () + +@end + +@implementation CustomObjcViewController + +- (void)viewDidLoad { + [super viewDidLoad]; +} + +@end diff --git a/DatadogCore/Tests/Helpers/DatadogExtensions.swift b/DatadogCore/Tests/Helpers/DatadogExtensions.swift new file mode 100644 index 0000000000..ff70cb5031 --- /dev/null +++ b/DatadogCore/Tests/Helpers/DatadogExtensions.swift @@ -0,0 +1,40 @@ +/* + * 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 + +/* + Set of Datadog domain extensions over standard types for writing more readable tests. + Domain agnostic extensions should be put in `SwiftExtensions.swift`. +*/ + +extension Date { + /// Returns name of the logs file created at this date. + var toFileName: String { + return fileNameFrom(fileCreationDate: self) + } +} + +extension File { + func makeReadonly() throws { + try FileManager.default.setAttributes([.immutable: true], ofItemAtPath: url.path) + } + + func makeReadWrite() throws { + try FileManager.default.setAttributes([.immutable: false], ofItemAtPath: url.path) + } + + /// Reads the file content and returns events data. It assumes that `self` is a batch file storing events in TLV format. + func readBatchEvents() throws -> [Data] { + let blocks = try BatchDataBlockReader(input: stream()).all() + return blocks.map { $0.data } + } + + func read() throws -> Data { + try Data(contentsOf: url) + } +} diff --git a/DatadogCore/Tests/Helpers/NSURLSessionBridge.h b/DatadogCore/Tests/Helpers/NSURLSessionBridge.h new file mode 100644 index 0000000000..9e34a003a2 --- /dev/null +++ b/DatadogCore/Tests/Helpers/NSURLSessionBridge.h @@ -0,0 +1,16 @@ +/* + * 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 + +@interface NSURLSessionBridge : NSObject + +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; + +- (id)init: (NSURLSession*)session; + +@end diff --git a/DatadogCore/Tests/Helpers/NSURLSessionBridge.m b/DatadogCore/Tests/Helpers/NSURLSessionBridge.m new file mode 100644 index 0000000000..b957cdb6d6 --- /dev/null +++ b/DatadogCore/Tests/Helpers/NSURLSessionBridge.m @@ -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 "NSURLSessionBridge.h" + +@interface NSURLSessionBridge() +@property NSURLSession *session; +@end + +@implementation NSURLSessionBridge + +- (id)init:(NSURLSession *)session { + self = [super init]; + if (self) { + self.session = session; + } + return self; +} + +- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler { + return [self.session dataTaskWithURL:url completionHandler:completionHandler]; +} + +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler { + return [self.session dataTaskWithRequest:request completionHandler:completionHandler]; +} + +@end diff --git a/Tests/DatadogTests/Matchers/JSONDataMatcher.swift b/DatadogCore/Tests/Matchers/JSONDataMatcher.swift similarity index 83% rename from Tests/DatadogTests/Matchers/JSONDataMatcher.swift rename to DatadogCore/Tests/Matchers/JSONDataMatcher.swift index c0603e7205..a7576cd626 100644 --- a/Tests/DatadogTests/Matchers/JSONDataMatcher.swift +++ b/DatadogCore/Tests/Matchers/JSONDataMatcher.swift @@ -1,11 +1,12 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import Foundation import XCTest +import TestUtilities /// Provides set of assertions for single JSON object or collection of JSON objects. /// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. @@ -90,6 +91,8 @@ internal class JSONDataMatcher { let description: String } + /// Returns value at given key-path by casting it to expected type. + /// Throws an error if value at given key-path does not exist. func value(forKeyPath keyPath: String) throws -> T { let dictionary = json as NSDictionary guard let anyValue = dictionary.value(forKeyPath: keyPath) else { @@ -104,24 +107,19 @@ internal class JSONDataMatcher { } return tValue } -} -internal extension Data { - func toArrayOfJSONObjects(file: StaticString = #file, line: UInt = #line) throws -> [[String: Any]] { - guard let jsonArray = try? JSONSerialization.jsonObject(with: self, options: []) as? [[String: Any]] else { - XCTFail("Cannot decode array of JSON objects from data.", file: file, line: line) - return [] + /// Returns value at given key-path by casting it to expected type. + /// Returns `nil` if no value at given key-path exist. + func valueOrNil(forKeyPath keyPath: String) throws -> T? { + let dictionary = json as NSDictionary + guard let anyValue = dictionary.value(forKeyPath: keyPath) else { + return nil } - - return jsonArray - } - - func toJSONObject(file: StaticString = #file, line: UInt = #line) throws -> [String: Any] { - guard let jsonObject = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: Any] else { - XCTFail("Cannot decode JSON object from given data.", file: file, line: line) - return [:] + guard let tValue = anyValue as? T else { + throw Exception( + description: "Cannot cast value for key path `\(keyPath)` to type `\(T.self)`: \(String(describing: anyValue))" + ) } - - return jsonObject + return tValue } } diff --git a/Tests/DatadogTests/Matchers/LogMatcher.swift b/DatadogCore/Tests/Matchers/LogMatcher.swift similarity index 78% rename from Tests/DatadogTests/Matchers/LogMatcher.swift rename to DatadogCore/Tests/Matchers/LogMatcher.swift index 970aa51628..be9b42b178 100644 --- a/Tests/DatadogTests/Matchers/LogMatcher.swift +++ b/DatadogCore/Tests/Matchers/LogMatcher.swift @@ -1,25 +1,27 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import XCTest +import TestUtilities -/// Provides set of assertions for single `Log` JSON object or collection of `[Log]`. -/// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. +/// Provides set of assertions for single `Log` JSON object and collection of `[Log]`. +/// Note: this file is individually referenced by integration tests target, so no dependency on other source files should be introduced. internal class LogMatcher: JSONDataMatcher { /// Log JSON keys. struct JSONKey { static let date = "date" static let status = "status" static let message = "message" - static let serviceName = "service" + static let service = "service" static let tags = "ddtags" // MARK: - Application info static let applicationVersion = "version" + static let applicationBuildNumber = "build_version" // MARK: - Logger info @@ -48,6 +50,18 @@ internal class LogMatcher: JSONDataMatcher { static let mobileNetworkCarrierISOCountryCode = "network.client.sim_carrier.iso_country" static let mobileNetworkCarrierRadioTechnology = "network.client.sim_carrier.technology" static let mobileNetworkCarrierAllowsVoIP = "network.client.sim_carrier.allows_voip" + + // MARK: - Error info + + static let errorKind = "error.kind" + static let errorMessage = "error.message" + static let errorStack = "error.stack" + static let errorFingerprint = "error.fingerprint" + + // MARK: - Dd info + static let dd = "_dd" + static let ddDevice = "device" + static let ddDeviceArchitecture = "architecture" } /// Allowed values for `network.client.available_interfaces` attribute. @@ -66,6 +80,14 @@ internal class LogMatcher: JSONDataMatcher { .map { LogMatcher(from: $0) } } + class func fromLogsRequest(_ request: URLRequest, file: StaticString = #file, line: UInt = #line) throws -> [LogMatcher] { + guard let body = try request.decompressed().httpBody else { + XCTFail("Request has no body", file: file, line: line) + return [] + } + return try fromArrayOfJSONObjectsData(body) + } + override private init(from jsonObject: [String: Any]) { super.init(from: jsonObject) } @@ -84,8 +106,8 @@ internal class LogMatcher: JSONDataMatcher { XCTAssertTrue(datePredicate(date), file: file, line: line) } - func assertServiceName(equals serviceName: String, file: StaticString = #file, line: UInt = #line) { - assertValue(forKey: JSONKey.serviceName, equals: serviceName, file: file, line: line) + func assertService(equals serviceName: String, file: StaticString = #file, line: UInt = #line) { + assertValue(forKey: JSONKey.service, equals: serviceName, file: file, line: line) } func assertThreadName(equals threadName: String, file: StaticString = #file, line: UInt = #line) { @@ -104,6 +126,10 @@ internal class LogMatcher: JSONDataMatcher { assertValue(forKey: JSONKey.applicationVersion, equals: applicationVersion, file: file, line: line) } + func assertApplicationBuildNumber(equals applicationBuildNumber: String, file: StaticString = #file, line: UInt = #line) { + assertValue(forKey: JSONKey.applicationBuildNumber, equals: applicationBuildNumber, file: file, line: line) + } + func assertStatus(equals status: String, file: StaticString = #file, line: UInt = #line) { assertValue(forKey: JSONKey.status, equals: status, file: file, line: line) } @@ -112,6 +138,10 @@ internal class LogMatcher: JSONDataMatcher { assertValue(forKey: JSONKey.message, equals: message, file: file, line: line) } + func assertErrorFingerprint(equals fingerprint: String) { + assertValue(forKey: JSONKey.errorFingerprint, equals: fingerprint) + } + func assertUserInfo(equals userInfo: (id: String?, name: String?, email: String?)?, file: StaticString = #file, line: UInt = #line) { if let id = userInfo?.id { assertValue(forKey: JSONKey.userId, equals: id, file: file, line: line) @@ -158,6 +188,16 @@ internal class LogMatcher: JSONDataMatcher { XCTAssertEqual(matcherTags, logTags, file: file, line: line) } + + func assertHasArchitecture() { + var architecture: String? + if let dd = json[JSONKey.dd] as? [String: Any], + let device = dd[JSONKey.ddDevice] as? [String: Any] { + architecture = device[JSONKey.ddDeviceArchitecture] as? String + } + + XCTAssertNotNil(architecture) + } } func date(fromISO8601FormattedString: String) -> Date? { diff --git a/DatadogCore/Tests/Matchers/RUMEventMatcher.swift b/DatadogCore/Tests/Matchers/RUMEventMatcher.swift new file mode 100644 index 0000000000..a1abf27cc4 --- /dev/null +++ b/DatadogCore/Tests/Matchers/RUMEventMatcher.swift @@ -0,0 +1,161 @@ +/* + * 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 XCTest + +/// Provides set of assertions for single `RUMDataModel` JSON object and collection of `[RUMDataModel]`. +/// Note: this file is individually referenced by integration tests target, so no dependency on other source files should be introduced except `RUMDataModel` implementations +/// for partial matching concrete RUM events conforming to [rum-events-format](https://github.com/DataDog/rum-events-format). +internal class RUMEventMatcher { + // MARK: - Initialization + + /// Returns "RUM event" matcher for data representing JSON string: + class func fromJSONObjectData(_ data: Data) throws -> RUMEventMatcher { + return try RUMEventMatcher(with: data) + } + + /// Returns array containing RUM event A, RUM event B RUM event Span C matchers for data representing string: + /// + /// ``` + /// { /* RUM event A json */ } + /// { /* RUM event B json */ } + /// { /* RUM event C json */ } + /// ``` + /// + /// **See Also** `RUMEventMatcher.fromJSONObjectData(_:)` + /// - Parameter data: payload data + /// - Parameter eventsPatch: optional transformation to apply on each event within the payload before instantiating matcher (default: `nil`) + class func fromNewlineSeparatedJSONObjectsData(_ data: Data, eventsPatch: ((Data) throws -> Data)? = nil) throws -> [RUMEventMatcher] { + let separator = "\n".data(using: .utf8)![0] + var eventsData = data.split(separator: separator).map { Data($0) } + if let patch = eventsPatch { + eventsData = try eventsData.map { try patch($0) } + } + return try eventsData.map { eventJSONData in try RUMEventMatcher.fromJSONObjectData(eventJSONData) } + } + + let jsonData: Data + let jsonMatcher: JSONDataMatcher + + private let jsonDataDecoder = JSONDecoder() + + private init(with jsonData: Data) throws { + self.jsonMatcher = JSONDataMatcher(from: try jsonData.toJSONObject()) + self.jsonData = jsonData + } + + // MARK: - Full match + + func assertItFullyMatches(jsonString: String, file: StaticString = #file, line: UInt = #line) throws { + try jsonMatcher.assertItFullyMatches(jsonString: jsonString, file: file, line: line) + } + + // MARK: - Partial matches + + func model(file: StaticString = #file, line: UInt = #line) throws -> DM { + do { + let model = try jsonDataDecoder.decode(DM.self, from: jsonData) + return model + } catch { + XCTFail("\(error)", file: file, line: line) + throw error + } + } + + func model(ofType type: DM.Type, file: StaticString = #file, line: UInt = #line, matches matcher: (DM) -> Void) throws { + do { + let model = try jsonDataDecoder.decode(DM.self, from: jsonData) + matcher(model) + } catch { + XCTFail("\(error)", file: file, line: line) + throw error + } + } + + func model(isTypeOf type: DM.Type) -> Bool { + return (try? jsonDataDecoder.decode(DM.self, from: jsonData)) != nil + } + + func eventType() throws -> String { try jsonMatcher.value(forKeyPath: "type") } + + func sessionHasReplay() throws -> Bool? { try jsonMatcher.valueOrNil(forKeyPath: "session.has_replay") } + + func userID() throws -> String { try jsonMatcher.value(forKeyPath: "usr.id") } + func userName() throws -> String { try jsonMatcher.value(forKeyPath: "usr.name") } + func userEmail() throws -> String { try jsonMatcher.value(forKeyPath: "usr.email") } + + func networkReachability() throws -> String { try jsonMatcher.value(forKeyPath: "meta.network.client.reachability") } + func networkAvailableInterfaces() throws -> [String] { try jsonMatcher.value(forKeyPath: "meta.network.client.available_interfaces") } + func networkConnectionSupportsIPv4() throws -> Bool { try jsonMatcher.value(forKeyPath: "meta.network.client.supports_ipv4") } + func networkConnectionSupportsIPv6() throws -> Bool { try jsonMatcher.value(forKeyPath: "meta.network.client.supports_ipv6") } + func networkConnectionIsExpensive() throws -> Bool { try jsonMatcher.value(forKeyPath: "meta.network.client.is_expensive") } + func networkConnectionIsConstrained() throws -> Bool { try jsonMatcher.value(forKeyPath: "meta.network.client.is_constrained") } + + func mobileNetworkCarrierName() throws -> String { try jsonMatcher.value(forKeyPath: "meta.network.client.sim_carrier.name") } + func mobileNetworkCarrierISOCountryCode() throws -> String { try jsonMatcher.value(forKeyPath: "meta.network.client.sim_carrier.iso_country") } + func mobileNetworkCarrierRadioTechnology() throws -> String { try jsonMatcher.value(forKeyPath: "meta.network.client.sim_carrier.technology") } + func mobileNetworkCarrierAllowsVoIP() throws -> Bool { try jsonMatcher.value(forKeyPath: "meta.network.client.sim_carrier.allows_voip") } + + func attribute(forKeyPath keyPath: String) throws -> T { + return try jsonMatcher.value(forKeyPath: keyPath) + } + + func timing(named timingName: String) throws -> Int64 { + return try attribute(forKeyPath: "view.custom_timings.\(timingName)") + } +} + +extension RUMEventMatcher: CustomStringConvertible { + /// Returns pretty JSON representation of this matcher. Handy for debugging with `po matcher`. + var description: String { + do { + let jsonObject = try jsonData.toJSONObject() + let prettyPrintedJSONData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]) + return String(data: prettyPrintedJSONData, encoding: .utf8) ?? "Failed to build String from utf8 data" + } catch { + return "Cannot build pretty JSON: \(error)" + } + } +} + +extension Array where Element == RUMEventMatcher { + func filterApplicationLaunchView() -> [RUMEventMatcher] { + return filter { + (try? $0.attribute(forKeyPath: "view.url")) != "com/datadog/application-launch/view" + } + } + + func filterTelemetry() -> [RUMEventMatcher] { + return filter { + (try? $0.attribute(forKeyPath: "type")) != "telemetry" + } + } + + func filterRUMEvents(ofType type: DM.Type, where predicate: ((DM) -> Bool)? = nil) -> [Element] { + return filter { matcher in matcher.model(isTypeOf: type) } + .filter { matcher in predicate?(try! matcher.model()) ?? true } + } + + func lastRUMEvent( + ofType type: DM.Type, + file: StaticString = #file, + line: UInt = #line, + where predicate: ((DM) -> Bool)? = nil + ) throws -> Element { + let last = filterRUMEvents(ofType: type, where: predicate).last + return try XCTUnwrap(last, "Cannot find RUMEventMatcher matching the predicate", file: file, line: line) + } + + func forEachRUMEvent(ofType type: DM.Type, body: ((DM) -> Void)) throws { + return try filter { matcher in matcher.model(isTypeOf: type) } + .forEach { matcher in body(try matcher.model()) } + } + + func compactMap(_ type: DM.Type) throws -> [DM] { + return try filter { $0.model(isTypeOf: type) }.map { try $0.model() } + } +} diff --git a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift new file mode 100644 index 0000000000..4c9fccc297 --- /dev/null +++ b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift @@ -0,0 +1,747 @@ +/* + * 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_COMPILED_FOR_INTEGRATION_TESTS +/// This file is compiled both for Unit and Integration tests. +/// * The Unit Tests target can see `Datadog` by `@testable import DatadogCore`. +/// * In Integration Tests target we want to compile `Datadog` in "Release" configuration, so testability is not possible. +/// This compiler statement gives both targets the visibility of `RUMDataModels.swift` either by import or direct compilation. +@testable import DatadogRUM +#endif + +/// An error thrown by the `RUMSessionMatcher` if it spots an inconsistency in tracked RUM Session, e.g. when +/// two RUM View events have the same `view.id` but different path (which is not allowed by the RUM product constraints). +struct RUMSessionConsistencyException: Error, CustomStringConvertible { + let description: String +} + +internal class RUMSessionMatcher { + // MARK: - Initialization + + /// Takes the array of `RUMEventMatchers` and groups them by application ID and session ID. + /// For each distinct session ID, the `RUMSessionMatcher` is created. + /// Each `RUMSessionMatcher` groups its RUM Events by kind and `ViewVisit`. + class func groupMatchersBySessions(_ matchers: [RUMEventMatcher]) throws -> [RUMSessionMatcher] { + struct Group: Hashable { + let applicationID: String + let sessionID: String + } + + let eventMatchersBySessionID: [Group: [RUMEventMatcher]] = try Dictionary(grouping: matchers) { eventMatcher in + Group( + applicationID: try eventMatcher.jsonMatcher.value(forKeyPath: "application.id"), + sessionID: try eventMatcher.jsonMatcher.value(forKeyPath: "session.id") + ) + } + + return try eventMatchersBySessionID + .map { group, eventMatchers in + try RUMSessionMatcher( + applicationID: group.applicationID, + sessionID: group.sessionID, + 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 + + /// Single RUM View visit tracked in this RUM Session. + /// Groups all the `RUMEvents` send during this visit. + class View { + /// The identifier of all `RUM Views` tracked during this visit. + let viewID: String + + init(viewID: String) { + self.viewID = viewID + } + + /// The `name` of the visited RUM View. + /// Corresponds to the "VIEW NAME" in RUM Explorer. + /// Might be `nil` for events received from Browser SDK. + fileprivate(set) var name: String? + + /// The `path` of the visited RUM View. + /// Corresponds to the "VIEW URL" in RUM Explorer. + fileprivate(set) var path: String = "" + + /// `RUMView` events tracked during this visit. + fileprivate(set) var viewEvents: [RUMViewEvent] = [] + + /// `RUMEventMatchers` corresponding to item in `viewEvents`. + fileprivate(set) var viewEventMatchers: [RUMEventMatcher] = [] + + /// `RUMAction` events tracked during this visit. + fileprivate(set) var actionEvents: [RUMActionEvent] = [] + + /// `RUMResource` events tracked during this visit. + fileprivate(set) var resourceEvents: [RUMResourceEvent] = [] + + /// `RUMError` events tracked during this visit. + fileprivate(set) var errorEvents: [RUMErrorEvent] = [] + + /// `RUMLongTask` events tracked during this visit. + fileprivate(set) var longTaskEvents: [RUMLongTaskEvent] = [] + } + + /// RUM application ID for this session. + let applicationID: String + /// The ID of this session in RUM. + let sessionID: String + + /// An array of view visits tracked during this RUM session. + /// Each `ViewVisit` is determined by unique `view.id` and groups all RUM events linked to that `view.id`.' + let views: [View] + + /// All RUM events in this session. + let allEvents: [RUMEventMatcher] + + let viewEventMatchers: [RUMEventMatcher] + let actionEventMatchers: [RUMEventMatcher] + let resourceEventMatchers: [RUMEventMatcher] + 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 + let firstEventTime: Int64 = try firstEvent.jsonMatcher.value(forKeyPath: "date") + let secondEventTime: Int64 = try secondEvent.jsonMatcher.value(forKeyPath: "date") + return firstEventTime < secondEventTime + } + + let eventsMatchersByType: [String: [RUMEventMatcher]] = try Dictionary(grouping: sessionEventOrderedByTime) { eventMatcher in + try eventMatcher.jsonMatcher.value(forKeyPath: "type") as String + } + + // Get RUM Events by kind: + + self.applicationID = applicationID + self.sessionID = sessionID + self.allEvents = sessionEventMatchers + self.viewEventMatchers = eventsMatchersByType["view"] ?? [] + self.actionEventMatchers = eventsMatchersByType["action"] ?? [] + self.resourceEventMatchers = eventsMatchersByType["resource"] ?? [] + self.errorEventMatchers = eventsMatchersByType["error"] ?? [] + self.longTaskEventMatchers = eventsMatchersByType["long_task"] ?? [] + + let viewEvents: [RUMViewEvent] = try viewEventMatchers.map { matcher in try matcher.model() } + + let actionEvents: [RUMActionEvent] = try actionEventMatchers + .map { matcher in try matcher.model() } + + let resourceEvents: [RUMResourceEvent] = try resourceEventMatchers + .map { matcher in try matcher.model() } + + let errorEvents: [RUMErrorEvent] = try errorEventMatchers + .map { matcher in try matcher.model() } + + let longTaskEvents: [RUMLongTaskEvent] = try longTaskEventMatchers + .map { matcher in try matcher.model() } + + // Validate each group of events individually + try validate(rumViewEvents: viewEvents) + try validate(rumActionEvents: actionEvents) + try validate(rumResourceEvents: resourceEvents) + try validate(rumErrorEvents: errorEvents) + try validate(rumLongTaskEvents: longTaskEvents) + + // Group RUMView events into ViewVisits: + let uniqueViewIDs = Set(viewEvents.map { $0.view.id }) + let visits = uniqueViewIDs.map { viewID in View(viewID: viewID) } + + var visitsByViewID: [String: View] = [:] + visits.forEach { visit in visitsByViewID[visit.viewID] = visit } + + // Group RUM Events and their matchers by View Visits: + try zip(viewEvents, viewEventMatchers).forEach { rumEvent, matcher in + if let visit = visitsByViewID[rumEvent.view.id] { + visit.viewEvents.append(rumEvent) + visit.viewEventMatchers.append(matcher) + if visit.name == nil { + visit.name = rumEvent.view.name + } else if visit.name != rumEvent.view.name { + throw RUMSessionConsistencyException( + description: "The RUM View name: \(rumEvent) is different than other RUM View names for the same `view.id`." + ) + } + if visit.path.isEmpty { + visit.path = rumEvent.view.url + } else if visit.path != rumEvent.view.url { + throw RUMSessionConsistencyException( + description: "The RUM View url: \(rumEvent) is different than other RUM View urls for the same `view.id`." + ) + } + } else { + throw RUMSessionConsistencyException( + description: "Cannot link RUM Event: \(rumEvent) to `RUMSessionMatcher.ViewVisit` by `view.id`." + ) + } + } + + try actionEvents.forEach { rumEvent in + if let visit = visitsByViewID[rumEvent.view.id] { + visit.actionEvents.append(rumEvent) + } else { + throw RUMSessionConsistencyException( + description: "Cannot link RUM Event: \(rumEvent) to `RUMSessionMatcher.ViewVisit` by `view.id`." + ) + } + } + + try resourceEvents.forEach { rumEvent in + if let visit = visitsByViewID[rumEvent.view.id] { + visit.resourceEvents.append(rumEvent) + } else { + throw RUMSessionConsistencyException( + description: "Cannot link RUM Event: \(rumEvent) to `RUMSessionMatcher.ViewVisit` by `view.id`." + ) + } + } + + try errorEvents.forEach { rumEvent in + if let visit = visitsByViewID[rumEvent.view.id] { + visit.errorEvents.append(rumEvent) + } else { + throw RUMSessionConsistencyException( + description: "Cannot link RUM Event: \(rumEvent) to `RUMSessionMatcher.ViewVisit` by `view.id`." + ) + } + } + + try longTaskEvents.forEach { rumEvent in + if let visit = visitsByViewID[rumEvent.view.id] { + visit.longTaskEvents.append(rumEvent) + } else { + throw RUMSessionConsistencyException( + description: "Cannot link RUM Event: \(rumEvent) to `RUMSessionMatcher.ViewVisit` by `view.id`." + ) + } + } + + // Sort visits by time + let visitsEventOrderedByTime = visits.sorted { firstVisit, secondVisit in + let firstVisitTime = firstVisit.viewEvents[0].date + let secondVisitTime = secondVisit.viewEvents[0].date + return firstVisitTime < secondVisitTime + } + + // Sort view events in each visit by document version + visits.forEach { visit in + visit.viewEvents = visit.viewEvents.sorted { viewUpdate1, viewUpdate2 in + viewUpdate1.dd.documentVersion < viewUpdate2.dd.documentVersion + } + } + + // Validate ViewVisit's view.isActive for each events + try visits.forEach { visit in + var viewIsInactive = false + try visit.viewEvents.enumerated().forEach { index, viewEvent in + guard let viewIsActive = viewEvent.view.isActive else { + throw RUMSessionConsistencyException( + description: "A `RUMSessionMatcher.ViewVisit` can't have an event without the `isActive` parameter set." + ) + } + + if viewIsInactive { + throw RUMSessionConsistencyException( + description: "A `RUMSessionMatcher.ViewVisit` can't have an event after the `View` was marked as inactive." + ) + } + viewIsInactive = !viewIsActive + } + } + + 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. + /// - Parameter viewID: The ID of the view to check. + /// - Returns: `true` if a view with the given `viewID` is present in this session; otherwise, `false`. + func containsView(with viewID: String) -> Bool { + let allIDs = Set(views.map { $0.viewID }) + return allIDs.contains(viewID) + } +} + +private func validate(rumViewEvents: [RUMViewEvent]) throws { + // All view events must use `session.plan` "lite" + try rumViewEvents.forEach { viewEvent in + if viewEvent.source == .ios { // validete only mobile events + try validate(device: viewEvent.device) + try validate(os: viewEvent.os) + } + } +} + +private func validate(rumActionEvents: [RUMActionEvent]) throws { + // All action events must use `session.plan` "lite" + try rumActionEvents.forEach { actionEvent in + if actionEvent.source == .ios { // validete only mobile events + try validate(device: actionEvent.device) + try validate(os: actionEvent.os) + } + } +} + +private func validate(rumResourceEvents: [RUMResourceEvent]) throws { + // All resource events must have unique ID + let ids = Set(rumResourceEvents.map { $0.resource.id }) + if ids.count != rumResourceEvents.count { + throw RUMSessionConsistencyException( + description: "`resource.id` should be unique - found at least two RUMResourceEvents with the same `resource.id`." + ) + } + + // All resource events must use `session.plan` "lite" + try rumResourceEvents.forEach { resourceEvent in + if resourceEvent.source == .ios { // validete only mobile events + try validate(device: resourceEvent.device) + try validate(os: resourceEvent.os) + } + } +} + +private func validate(rumErrorEvents: [RUMErrorEvent]) throws { + // All error events must use `session.plan` "lite" + try rumErrorEvents.forEach { errorEvent in + if errorEvent.source == .ios { // validete only mobile events + try validate(device: errorEvent.device) + try validate(os: errorEvent.os) + } + } +} + +private func validate(rumLongTaskEvents: [RUMLongTaskEvent]) throws { + // All error events must use `session.plan` "lite" + try rumLongTaskEvents.forEach { longTaskEvent in + if longTaskEvent.source == .ios { // validete only mobile events + try validate(device: longTaskEvent.device) + try validate(os: longTaskEvent.os) + } + } +} + +private func validate(device: RUMDevice?) throws { + guard let device = device else { + throw RUMSessionConsistencyException( + description: "All RUM events must include device information" + ) + } + #if DD_COMPILED_FOR_INTEGRATION_TESTS + try strictValidate(device: device) + #endif +} + +private func validate(os: RUMOperatingSystem?) throws { + guard let os = os else { + throw RUMSessionConsistencyException( + description: "All RUM events must include OS information" + ) + } + #if DD_COMPILED_FOR_INTEGRATION_TESTS + try strictValidate(os: os) + #endif +} + +// MARK: - Strict Validation (in Integration Tests) + +/// Performs strict validation of `RUMDevice` for integration tests. +/// It asserts that all values make sense for current environment. +private func strictValidate(device: RUMDevice) throws { + guard device.brand == "Apple" else { + throw RUMSessionConsistencyException(description: "All RUM events must use `device.brand = Apple` (got `\(device.brand ?? "nil")` instead)") + } + #if os(iOS) + guard device.type == .mobile || device.type == .tablet else { + throw RUMSessionConsistencyException( + description: "When running on iOS or iPadOS, the `device.type` must be `.mobile` or `.tablet` (got `\(device.type)` instead)" + ) + } + let prefixes = ["iPhone", "iPod", "iPad"] + guard prefixes.contains(where: { device.name?.hasPrefix($0) ?? false }) else { + throw RUMSessionConsistencyException( + description: "When running on iOS or iPadOS, the `device.name` must start with one of: \(prefixes) (got `\(device.name ?? "nil")` instead)" + ) + } + guard prefixes.contains(where: { device.model?.hasPrefix($0) ?? false }) else { + throw RUMSessionConsistencyException( + description: "When running on iOS or iPadOS, the `device.model` must start with one of: \(prefixes) (got `\(device.model ?? "nil")` instead)" + ) + } + #else + guard device.type != .tv else { + throw RUMSessionConsistencyException( + description: "When running on tvOS, the `device.type` must be `.tv` (got `\(device.type)` instead)" + ) + } + guard device.name == "Apple TV" else { + throw RUMSessionConsistencyException( + description: "When running on tvOS, the `device.name` must be `Apple TV` (got `\(device.name ?? "nil")` instead)" + ) + } + guard device.model?.hasPrefix("AppleTV") ?? false else { + throw RUMSessionConsistencyException( + description: "When running on tvOS, the `device.model` must start with `AppleTV` (got `\(device.model ?? "nil")` instead)" + ) + } + #endif +} + +/// Performs strict validation of `RUMOperatingSystem` for integration tests. +/// It asserts that all values make sense for current environment. +private func strictValidate(os: RUMOperatingSystem) throws { + #if os(iOS) + guard os.name == "iOS" || os.name == "iPadOS" else { + throw RUMSessionConsistencyException( + description: "When running on iOS or iPadOS the `os.name` must be either 'iOS' or 'iPadOS'" + ) + } + #else + guard os.name == "tvOS" else { + throw RUMSessionConsistencyException( + description: "When running on tvOS the `os.name` must be 'tvOS'" + ) + } + #endif +} + +// MARK: - Matching + +extension Array where Element == RUMSessionMatcher { + /// Returns the only session in this array. + /// Throws if there is more than one session or the array has no elements. + func takeSingle() throws -> RUMSessionMatcher { + guard !isEmpty else { + throw RUMSessionConsistencyException(description: "There are no sessions in this array") + } + guard count == 1 else { + throw RUMSessionConsistencyException(description: "Expected to find only one session, but found \(count)") + } + 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 { + /// Returns list of views by dropping "application launch" view. + /// Throws if "application launch" is not the first view in this array. + /// + /// Use it to explicitly ignore the "application launch" view with running strict check of its existence. + func dropApplicationLaunchView() throws -> [RUMSessionMatcher.View] { + guard let first = first else { + throw RUMSessionConsistencyException(description: "Cannot drop 'application launch' view in empty array") + } + guard first.isApplicationLaunchView() else { + throw RUMSessionConsistencyException(description: "The first view in this array is not 'application launch' view (\(first.name ?? "???")") + } + return Array(dropFirst()) + } +} + +extension RUMSessionMatcher.View { + /// Whether this is "application launch" view. + func isApplicationLaunchView() -> Bool { + return name == "ApplicationLaunch" && path == "com/datadog/application-launch/view" + } + + /// Whether this is "background" view. + func isBackgroundView() -> Bool { + return name == "Background" && path == "com/datadog/background/view" + } +} + +private extension Date { + init(millisecondsSince1970: Int64) { + self.init(timeIntervalSince1970: TimeInterval(millisecondsSince1970) / 1_000) + } +} + +private extension TimeInterval { + init(fromNanoseconds nanoseconds: Int64) { + self = TimeInterval(nanoseconds) / 1_000_000_000 + } +} + +extension RUMSessionMatcher { + /// Asserts that all events in this session have certain `sessionPrecondition` set. + /// Throws if there are no views in this session. + func has(sessionPrecondition: RUMSessionPrecondition) throws -> Bool { + guard !views.isEmpty else { + throw RUMSessionConsistencyException(description: "There are no views in this session") + } + + for view in views { + guard view.viewEvents.allSatisfy({ $0.dd.session?.sessionPrecondition == sessionPrecondition }) else { + return false + } + guard view.actionEvents.allSatisfy({ $0.dd.session?.sessionPrecondition == sessionPrecondition }) else { + return false + } + guard view.resourceEvents.allSatisfy({ $0.dd.session?.sessionPrecondition == sessionPrecondition }) else { + return false + } + guard view.errorEvents.allSatisfy({ $0.dd.session?.sessionPrecondition == sessionPrecondition }) else { + return false + } + guard view.longTaskEvents.allSatisfy({ $0.dd.session?.sessionPrecondition == sessionPrecondition }) else { + return false + } + } + + return true + } +} + +// 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 { 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 + output += render(view: view) + } + output += renderClosingLine() + return output + } + + private func render(view: View) -> String { + guard let lastViewEvent = view.viewEvents.last else { + return renderBox(string: "⛔️ Invalid View - it has no view events") + } + + 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))"), + ] + ) + + for action in view.actionEvents { + output += renderEmptyLine() + output += render(event: action, in: view) + } + + for resource in view.resourceEvents { + output += renderEmptyLine() + output += render(event: resource, in: view) + } + + for error in view.errorEvents { + output += renderEmptyLine() + output += render(event: error, in: view) + } + + for longTask in view.longTaskEvents { + output += renderEmptyLine() + output += render(event: longTask, in: view) + } + + 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 + } + + // 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 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 renderEmptyLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "|" + String(repeating: " ", count: width - 2) + "|" + return horizontalBorder + "\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 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/DatadogCore/Tests/Matchers/SRRequestMatcher.swift b/DatadogCore/Tests/Matchers/SRRequestMatcher.swift new file mode 100644 index 0000000000..59935c334b --- /dev/null +++ b/DatadogCore/Tests/Matchers/SRRequestMatcher.swift @@ -0,0 +1,220 @@ +/* + * 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 TestUtilities + +private enum SRRequestException: Error { + case multipartRequestException(String) + case multipartDataException(String) + case multipartFormException(String) + case segmentException(String) +} + +/// Matcher for asserting known elements of Session Replay (multipart) request. +/// +/// See: ``DatadogSessionReplay.RequestBuilder`` to understand the encoding of multipart data operated by this matcher. +internal struct SRRequestMatcher { + /// Creates matcher from Session Replay `URLRequest`. + /// The `request` must be a valid Session Replay (multipart) request. + /// + /// - Parameter request: Session Replay request. + init(request: URLRequest) throws { + guard let body = request.httpBody else { + throw SRRequestException.multipartRequestException("Request must define body") + } + try self.init(body: body, headers: request.allHTTPHeaderFields ?? [:]) + } + + /// Creates matcher from request body and headers. + /// Both `body` and `headers` must describe a valid Session Replay (multipart) request. + /// + /// - Parameters: + /// - body: The body of request. + /// - headers: Request headers. + init(body: Data, headers: [String: String]) throws { + let contentTypePrefix = "multipart/form-data; boundary=" + guard let contentType = headers["Content-Type"] else { + throw SRRequestException.multipartRequestException("Request must define Content-Type header") + } + guard contentType.hasPrefix(contentTypePrefix) else { + throw SRRequestException.multipartRequestException("Content-Type must start with `\(contentTypePrefix)` and must specify boundary") + } + let boundary = contentType.removingPrefix(contentTypePrefix) + guard !boundary.isEmpty else { + throw SRRequestException.multipartRequestException("Multipart boundary must be a non-empty string") + } + try self.init(multipartBody: body, multipartBoundary: boundary) + } + + /// Underlying (multipart) form data sent with tested request. + private let multipartForm: MultipartFormDataParser + + /// Creates matcher from HTTP multipart data and given boundary. + /// - Parameters: + /// - multipartBody: The multipart HTTP body. + /// - multipartBoundary: The boundary encoded in `multipartBody`. + init(multipartBody: Data, multipartBoundary: String) throws { + self.multipartForm = try MultipartFormDataParser(data: multipartBody, boundary: multipartBoundary) + } + + /// Returns the blob file. + func blob(_ transform: (Data) throws -> T) throws -> T { + let data = try dataOfFile(named: "blob", fieldName: "event", mimeType: "application/json") + return try transform(data) + } + + /// Data of "segment" file in underlying multipart form. + func segment(at index: Int) throws -> SRSegmentMatcher { + let compressedData = try dataOfFile(named: "file\(index)", fieldName: "segment", mimeType: "application/octet-stream") + guard let data = zlib.decode(compressedData) else { + throw SRRequestException.segmentException("Failed to decompress segment JSON data: \(compressedData)") + } + + let object = try data.toJSONObject() + return SRSegmentMatcher(object: object) + } + + // MARK: - Querying Multipart Fields and Files + + private func valueOfField(named fieldName: String) throws -> String { + let contentDispositionHeader = "Content-Disposition: form-data; name=\"\(fieldName)\"" + let field = try part(with: [contentDispositionHeader]) + return field.message.utf8String + } + + private func dataOfFile(named fileName: String, fieldName: String, mimeType: String) throws -> Data { + let contentDispositionHeader = "Content-Disposition: form-data; name=\"\(fieldName)\"; filename=\"\(fileName)\"" + let contentTypeHeader = "Content-Type: \(mimeType)" + let field = try part(with: [contentDispositionHeader, contentTypeHeader]) + return field.message + } + + private func part(with headers: Set) throws -> MultipartFormDataParser.Part { + guard let match = multipartForm.parts.first(where: { part in headers.isSubset(of: Set(part.headers)) }) else { + throw SRRequestException.multipartFormException("No part in multipart form contains expected headers: '\(headers.joined(separator: ", "))'") + } + return match + } +} + +// MARK: - Multipart Parsing + +/// Basic parser for HTTP multipart data. +/// +/// It supports multipart idoms used in ``DatadogSessionReplay.MultipartFormData``. Other generic capabilities of +/// multipart format may not work correctly. Ref.: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html +private class MultipartFormDataParser { + private let cr: UInt8 = 13 // CR + private let lf: UInt8 = 10 // LF + private let delimiterBytes: [UInt8] + private let closingDelimiterBytes: [UInt8] + + private var bytes: [UInt8] + private var offset: Int = 0 + + struct Part { + let headers: [String] + let message: Data + } + + private(set) var parts: [Part] = [] + + init(data: Data, boundary: String) throws { + self.delimiterBytes = "--\(boundary)".utf8Bytes + [cr, lf] + self.closingDelimiterBytes = "--\(boundary)--".utf8Bytes + self.bytes = [UInt8](data) + try parseNext() + } + + private func parseNext() throws { + if try nextBytesEqual(delimiterBytes) { + try parseBody() + } else if try !nextBytesEqual(closingDelimiterBytes) { + throw exception("Unexpected bytes") + } + } + + /// Parses next part delimited by `delimiterBytes`. + private func parseBody() throws { + try seek(delimiterBytes.count) // skip delimiter + + // Read headers: + var headers: [String] = [] + while try !nextBytesEqual([cr, lf]) { + if let header = try readHeader() { + headers.append(header) + } + } + try seek(2) + + // Read message: + let message = try readMessage() + + // Extract new part: + parts.append(Part(headers: headers, message: message)) + + try parseNext() + } + + /// Reads headers of part delimited by `delimiterBytes`. + private func readHeader() throws -> String? { + var header: [UInt8] = [] + while try !nextBytesEqual([cr, lf]) { + header += try nextBytes(1) + try seek(1) + } + try seek(2) // skip CRLF + return header.isEmpty ? nil : Data(header).utf8String + } + + /// Reads message (body) of part delimited by `delimiterBytes`. + private func readMessage() throws -> Data { + var message: [UInt8] = [] + while try !nextBytesEqual([cr, lf] + delimiterBytes) && !nextBytesEqual([cr, lf] + closingDelimiterBytes) { + message += try nextBytes(1) + try seek(1) + } + try seek(2) // skip CRLF + return Data(message) + } + + // MARK: - Helpers + + private func nextBytesEqual(_ otherBytes: [UInt8]) throws -> Bool { + return (try? nextBytes(otherBytes.count)) == otherBytes + } + + private func nextBytes(_ count: Int) throws -> [UInt8] { + guard (offset + count) <= bytes.count else { + throw exception("can't get next \(count) bytes - reached the end of data") + } + return Array(bytes[offset..<(offset + count)]) + } + + private func seek(_ size: Int) throws { + guard (offset + size) <= bytes.count else { + throw exception("can't seek by \(size) - it will exceed data size") + } + return offset += size + } + + private func exception(_ message: String) -> Error { + let before = bytes[max(0, offset - 10).. String { try value("segment") } + + /// The value of "application.id" field in underlying multipart form. + func applicationID() throws -> String { try value("application.id") } + + /// The value of "session.id" field in underlying multipart form. + func sessionID() throws -> String { try value("session.id") } + + /// The value of "view.id" field in underlying multipart form. + func viewID() throws -> String { try value("view.id") } + + /// The value of "has_full_snapshot" field in underlying multipart form. + func hasFullSnapshot() throws -> Bool { try value("has_full_snapshot") } + + /// The value of "records_count" field in underlying multipart form. + func recordsCount() throws -> Int { try value("records_count") } + + /// The value of "raw_segment_size" field in underlying multipart form. + func rawSegmentSize() throws -> Int { try value("raw_segment_size") } + + /// The value of "compressed_segment_size" field in underlying multipart form. + func compressedSegmentSize() throws -> Int { try value("compressed_segment_size") } + + /// The value of "start" field in underlying multipart form. + func start() throws -> Int { try value("start") } + + /// The value of "end" field in underlying multipart form. + func end() throws -> Int { try value("end") } + + /// The value of "source" field in underlying multipart form. + func source() throws -> String { try value("source") } + + /// Returns an array of JSON object matchers for all records in this segment. + func records() throws -> [JSONObjectMatcher] { + try array("records").objects() + } + + /// Returns an array of JSON object matchers for records of a specific type. + /// - Parameter type: The type of records to retrieve. + /// - Returns: An array of `JSONObjectMatcher` instances representing the records of the specified type. + func records(type: RecordType) throws -> [JSONObjectMatcher] { + try records().filter { try $0.value("type") == type.rawValue } + } + + /// Returns an array of specialised matchers for "full snapshot" records. + func fullSnapshotRecords() throws -> [SRFullSnapshotRecordMatcher] { + try records(type: .fullSnapshotRecord).map { SRFullSnapshotRecordMatcher(jsonObject: $0.object) } + } + + /// Returns an array of specialised matchers for "incremental snapshot" records. + func incrementalSnapshotRecords() throws -> [SRIncrementalSnapshotRecordMatcher] { + try records(type: .incrementalSnapshotRecord).map { SRIncrementalSnapshotRecordMatcher(jsonObject: $0.object) } + } +} + +/// Matcher for asserting known values of Session Replay "full snapshot" record. +/// +/// See: ``DatadogSessionReplay.SRFullSnapshotRecord`` to understand how underlying data is encoded. +internal class SRFullSnapshotRecordMatcher: JSONObjectMatcher { + init(jsonObject: [String: Any]) { + super.init(object: jsonObject) + } + + /// Returns an array of JSON object matchers for wireframes contained in this record. + func wireframes() throws -> [JSONObjectMatcher] { + try array("data.wireframes").objects() + } +} + +/// Matcher for asserting known values of Session Replay "incremental snapshot" record. +/// +/// See: ``DatadogSessionReplay.SRIncrementalSnapshotRecord`` to understand how underlying data is encoded. +internal class SRIncrementalSnapshotRecordMatcher: JSONObjectMatcher { + init(jsonObject: [String: Any]) { + super.init(object: jsonObject) + } + + /// Enumerates data types in incremental snapshot. + /// Raw values correspond to types defined in SR JSON schema. + /// + /// See: ``DatadogSessionReplay.SRIncrementalSnapshotRecord.Data`` + enum IncrementalDataType: Int { + case mutationData = 0 + case touchData = 2 + case viewportResizeData = 4 + case pointerInteractionData = 9 + } + + func has(incrementalDataType: IncrementalDataType) throws -> Bool { + return try value("data.source") == incrementalDataType.rawValue + } +} diff --git a/Tests/DatadogTests/Matchers/SpanMatcher.swift b/DatadogCore/Tests/Matchers/SpanMatcher.swift similarity index 81% rename from Tests/DatadogTests/Matchers/SpanMatcher.swift rename to DatadogCore/Tests/Matchers/SpanMatcher.swift index 10f5042b39..755a7c2f95 100644 --- a/Tests/DatadogTests/Matchers/SpanMatcher.swift +++ b/DatadogCore/Tests/Matchers/SpanMatcher.swift @@ -1,13 +1,16 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ import Foundation +import DatadogInternal /// Implemented by types allowed to represent span attribute `.*` value in JSON. protocol AllowedSpanAttributeValue {} +/// Implemented by types allowed to represent span attribute `_dd.*` value in JSON. +protocol AllowedSpanDdValue {} /// Implemented by types allowed to represent span `metrics.*` value in JSON. protocol AllowedSpanMetricValue {} /// Implemented by types allowed to represent span `meta.*` value in JSON. @@ -18,14 +21,17 @@ extension String: AllowedSpanAttributeValue {} extension UInt64: AllowedSpanAttributeValue {} extension Int: AllowedSpanAttributeValue {} +// Only numeric values are allowed for `span._dd.*`. +extension Double: AllowedSpanDdValue {} + // Only numeric values are allowed for `span.metrics.*`. extension Int: AllowedSpanMetricValue {} // Only string values are allowed for `span.meta.*`. extension String: AllowedSpanMetaValue {} -/// Provides set of assertions for single `Span` JSON object or collection of `[Span]`. -/// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. +/// Provides set of assertions for single `SpanEvent` JSON object and collection of `[SpanEvent]`. +/// Note: this file is individually referenced by integration tests target, so no dependency on other source files should be introduced. internal class SpanMatcher { // MARK: - Initialization @@ -76,9 +82,26 @@ internal class SpanMatcher { // MARK: - Attributes matching - func traceID() throws -> String { try attribute(forKeyPath: "trace_id") } - func spanID() throws -> String { try attribute(forKeyPath: "span_id") } - func parentSpanID() throws -> String { try attribute(forKeyPath: "parent_id") } + func traceID() throws -> TraceID? { + let idLoStr: String = try attribute(forKeyPath: "trace_id") + let idLo = UInt64(idLoStr, radix: 16) ?? UInt64(0) + + let idHiStr: String = try meta.tid() + let idHi = UInt64(idHiStr, radix: 16) ?? UInt64(0) + + return .init(idHi: idHi, idLo: idLo) + } + + func spanID() throws -> SpanID? { + let spanId: String = try attribute(forKeyPath: "span_id") + return .init(spanId, representation: .hexadecimal) + } + + func parentSpanID() throws -> SpanID? { + let spanId: String = try attribute(forKeyPath: "parent_id") + return .init(spanId, representation: .hexadecimal) + } + func operationName() throws -> String { try attribute(forKeyPath: "name") } func serviceName() throws -> String { try attribute(forKeyPath: "service") } func resource() throws -> String { try attribute(forKeyPath: "resource") } @@ -88,6 +111,16 @@ internal class SpanMatcher { func isError() throws -> Int { try attribute(forKeyPath: "error") } func environment() throws -> String { try envelope.value(forKeyPath: "env") } + // MARK: - _dd matching + + var dd: Dd { Dd(matcher: self) } + + struct Dd { + fileprivate let matcher: SpanMatcher + + func samplingRate() throws -> Double { try matcher.dd(forKeyPath: "_dd.agent_psr") } + } + // MARK: - Metrics matching var metrics: Metrics { Metrics(matcher: self) } @@ -106,6 +139,7 @@ internal class SpanMatcher { struct Meta { fileprivate let matcher: SpanMatcher + func tid() throws -> String { try matcher.meta(forKeyPath: "meta._dd.p.tid") } func source() throws -> String { try matcher.meta(forKeyPath: "meta._dd.source") } func applicationVersion() throws -> String { try matcher.meta(forKeyPath: "meta.version") } func tracerVersion() throws -> String { try matcher.meta(forKeyPath: "meta.tracer.version") } @@ -142,6 +176,11 @@ internal class SpanMatcher { return try span.value(forKeyPath: keyPath) } + private func dd(forKeyPath keyPath: String) throws -> T { + precondition(keyPath.hasPrefix("_dd.")) + return try span.value(forKeyPath: keyPath) + } + private func metric(forKeyPath keyPath: String) throws -> T { precondition(keyPath.hasPrefix("metrics.")) return try span.value(forKeyPath: keyPath) diff --git a/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift b/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift new file mode 100644 index 0000000000..117de6c64b --- /dev/null +++ b/DatadogCore/Tests/TestsObserver/DatadogTestsObserver.swift @@ -0,0 +1,181 @@ +/* + * 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 + +/// Observes unit tests execution and performs integrity checks after each test to ensure that the global state is unaltered. +@objc +internal class DatadogTestsObserver: NSObject, XCTestObservation { + @objc + static func startObserving() { + let observer = DatadogTestsObserver() + XCTestObservationCenter.shared.addTestObserver(observer) + } + + // MARK: - Checking Tests Integrity + + /// A list of checks ensuring global state integrity before and after each tests. + private let checks: [TestIntegrityCheck] = [ + .init( + assert: { CoreRegistry.instances.isEmpty }, + problem: "No instance of `DatadogCore` must be left initialized after test completion.", + solution: """ + Make sure deinitialization APIs are called before the end of test that registers `DatadogCore`. + If registering directly to `CoreRegistry`, make sure the test cleans it up properly. + + `DatadogTestsObserver` found following instances still being registered: \(CoreRegistry.instances.map({ "'\($0.key)'" })) + """ + ), + .init( + assert: { Swizzling.methods.isEmpty }, + problem: "No swizzling must be applied.", + solution: """ + Make sure all applied swizzling are reset by the end of test with `unswizzle()`. + + `DatadogTestsObserver` found \(Swizzling.methods.count) leaked swizzlings: + \(Swizzling.description) + """ + ), + .init( + assert: { DD.logger is InternalLogger }, + problem: "`DD.logger` must use `InternalLogger` implementation.", + solution: """ + Make sure the `DD` bundle is reset after test to use previous dependencies, e.g.: + + ``` + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + ``` + """ + ), + .init( + assert: { ServerMock.activeInstance == nil }, + problem: "`ServerMock` must not be active.", + solution: """ + Make sure that test waits for `ServerMock` completion at the end: + + ``` + let server = ServerMock(...) + + // ... testing + + server.wait<...>(...) // <-- after return, no reference to `server` will exist as it processed all callbacks and got be safely deallocated + ``` + """ + ), + .init( + assert: { !FileManager.default.fileExists(atPath: temporaryDirectory.path) }, + problem: "`temporaryDirectory` must not exist.", + solution: """ + Make sure `DeleteTemporaryDirectory()` is called consistently + with `CreateTemporaryDirectory()`. + """ + ), + .init( + assert: { !temporaryCoreDirectory.coreDirectory.exists() + && !temporaryCoreDirectory.osDirectory.exists() + }, + problem: "`temporaryCoreDirectory` must not exist.", + solution: """ + Make sure `temporaryCoreDirectory.delete()` is called consistently + with `temporaryCoreDirectory.create()`. + """ + ), + .init( + assert: { + !temporaryFeatureDirectories.authorized.exists() + && !temporaryFeatureDirectories.unauthorized.exists() + }, + problem: "`temporaryFeatureDirectories` must not exist.", + solution: """ + Make sure that `temporaryFeatureDirectories` is unifromly managed in every test by using: + ``` + // Before test: + temporaryFeatureDirectories.create() + + // After test: + temporaryFeatureDirectories.delete() + ``` + """ + ), + .init( + assert: { DatadogCoreProxy.referenceCount == 0 }, + problem: "Leaking reference to `DatadogCoreProtocol`", + solution: """ + There should be no remaining reference to `DatadogCoreProtocol` upon each test completion + but some instances of `DatadogCoreProxy` are still alive. + + Make sure the instance of `DatadogCoreProxy` is properly managed in test: + - it must be allocated on each test start (e.g. in `setUp()` or directly in test) + - it must be flushed and deinitialized before test ends with `.flushAndTearDown()` + - it must be deallocated before test ends (e.g. in `tearDown()`) + + If all above conditions are met, this failure might indicate a memory leak in the implementation. + """ + ), + .init( + assert: { PassthroughCoreMock.referenceCount == 0 }, + problem: "Leaking reference to `DatadogCoreProtocol`", + solution: """ + There should be no remaining reference to `DatadogCoreProtocol` upon each test completion + but some instances of `PassthroughCoreMock` are still alive. + + Make sure the instance of `PassthroughCoreMock` is properly managed in test: + - it must be allocated on each test test start (e.g. in `setUp()` or directly in test) + - it must be deallocated before test ends (e.g. in `tearDown()`) + + If all above conditions are met, this failure might indicate a memory leak in the implementation. + """ + ) + ] + + func testCaseDidFinish(_ testCase: XCTestCase) { + if testCase.testRun?.hasSucceeded == true { + performIntegrityChecks(after: testCase) + } + } + + private func performIntegrityChecks(after testCase: XCTestCase) { + let failedChecks = checks.filter { $0.assert() == false } + + if !failedChecks.isEmpty { + var message = """ + 🐶✋ `DatadogTests` integrity check failure. + + `DatadogTestsObserver` found that `\(testCase.name)` breaks \(failedChecks.count) integrity rule(s) which + must be fulfilled before and after each unit test. Find potential root cause analysis below and try running + surrounding tests in isolation to pinpoint the issue: + """ + failedChecks.forEach { check in + message += """ + \n⚠️ ---- \(check.problem) ---- + 🔎 \(check.solution()) + """ + } + + message += "\n" + preconditionFailure(message) + } + } +} + +private struct TestIntegrityCheck { + /// If this assertion evaluates to `false`, the integrity issue is raised. + let assert: () -> Bool + /// What is the assertion about? + let problem: StaticString + /// How to fix it if it fails? + let solution: () -> String + + init(assert: @escaping () -> Bool, problem: StaticString, solution: @escaping @autoclosure () -> String) { + self.assert = assert + self.problem = problem + self.solution = solution + } +} diff --git a/DatadogCore/Tests/TestsObserver/DatadogTestsObserverLoader.m b/DatadogCore/Tests/TestsObserver/DatadogTestsObserverLoader.m new file mode 100644 index 0000000000..e2abc45f88 --- /dev/null +++ b/DatadogCore/Tests/TestsObserver/DatadogTestsObserverLoader.m @@ -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 +#import + +#if TARGET_OS_IOS +#import "DatadogCoreTests_iOS-Swift.h" +#elif TARGET_OS_TV +#import "DatadogCoreTests_tvOS-Swift.h" +#endif + +/// This code runs when the `DatadogTests` bundle is loaded into memory and tests start. +/// Reference: https://developer.apple.com/documentation/objectivec/nsobject/1418815-load +__attribute__((constructor)) static void initialize_FrameworkLoadHandler(void) { + [DatadogTestsObserver startObserving]; +} diff --git a/DatadogCrashReporting.podspec b/DatadogCrashReporting.podspec new file mode 100644 index 0000000000..04c17f9a56 --- /dev/null +++ b/DatadogCrashReporting.podspec @@ -0,0 +1,31 @@ +Pod::Spec.new do |s| + s.name = "DatadogCrashReporting" + s.version = "2.22.0" + s.summary = "Official Datadog Crash Reporting SDK for iOS." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + s.swift_version = '5.9' + s.ios.deployment_target = '12.0' + s.tvos.deployment_target = '12.0' + + s.source = { :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => s.version.to_s } + s.static_framework = true + + s.source_files = "DatadogCrashReporting/Sources/**/*.swift" + s.dependency 'DatadogInternal', s.version.to_s + s.dependency 'PLCrashReporter', '~> 1.11.2' + + s.resource_bundle = { + "DatadogCrashReporting" => "DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy" + } +end diff --git a/DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy b/DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..9264a9cd06 --- /dev/null +++ b/DatadogCrashReporting/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,21 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + diff --git a/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift b/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift new file mode 100644 index 0000000000..c55b307da6 --- /dev/null +++ b/DatadogCrashReporting/Sources/CrashContext/CrashContext.swift @@ -0,0 +1,168 @@ +/* + * 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 + +/// Describes current Datadog SDK context, so the app state information can be attached to +/// the crash report and retrieved back when the application is started again. +/// +/// Note: as it gets saved along with the crash report during process interruption, it's good +/// to keep this data well-packed and as small as possible. +internal struct CrashContext: Codable, Equatable { + /// The Application Launch Date + var appLaunchDate: Date? + + /// Interval between device and server time. + /// + /// The value can change as the device continue to sync with the server. + let serverTimeOffset: TimeInterval + + /// The name of the service that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + let service: String + + /// The name of the environment that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + let env: String + + /// The version of the application that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + let version: String + + /// The build number of the application that data is generated from. + let buildNumber: String + + /// Current device information. + let device: DeviceInfo + + /// The version of Datadog iOS SDK. + let sdkVersion: String + + /// Denotes the mobile application's platform, such as `"ios"` or `"flutter"` that data is generated from. + /// - See: Datadog [Reserved Attributes](https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#reserved-attributes). + let source: String + + /// The user's consent to data collection + let trackingConsent: TrackingConsent + + /// Current user information. + let userInfo: UserInfo? + + /// Network information. + /// + /// Represents the current state of the device network connectivity and interface. + /// The value can be `unknown` if the network interface is not available or if it has not + /// yet been evaluated. + let networkConnectionInfo: NetworkConnectionInfo? + + /// Carrier information. + /// + /// Represents the current telephony service info of the device. + /// This value can be `nil` of no service is currently registered, or if the device does + /// not support telephony services. + let carrierInfo: CarrierInfo? + + /// The last _"Is app in foreground?"_ information from crashed app process. + let lastIsAppInForeground: Bool + + /// The last RUM view in crashed app process. + var lastRUMViewEvent: AnyCodable? + + /// State of the last RUM session in crashed app process. + var lastRUMSessionState: AnyCodable? + + /// Last global log attributes, set with Logs.addAttribute / Logs.removeAttribute + var lastLogAttributes: AnyCodable? + + /// Last global RUM attributes. It gets updated with adding or removing attributes on `RUMMonitor`. + var lastRUMAttributes: GlobalRUMAttributes? + + // MARK: - Initialization + + init( + serverTimeOffset: TimeInterval, + service: String, + env: String, + version: String, + buildNumber: String, + device: DeviceInfo, + sdkVersion: String, + source: String, + trackingConsent: TrackingConsent, + userInfo: UserInfo?, + networkConnectionInfo: NetworkConnectionInfo?, + carrierInfo: CarrierInfo?, + lastIsAppInForeground: Bool, + appLaunchDate: Date?, + lastRUMViewEvent: AnyCodable?, + lastRUMSessionState: AnyCodable?, + lastRUMAttributes: GlobalRUMAttributes?, + lastLogAttributes: AnyCodable? + ) { + self.serverTimeOffset = serverTimeOffset + self.service = service + self.env = env + self.version = version + self.buildNumber = buildNumber + self.device = device + self.sdkVersion = service + self.source = source + self.trackingConsent = trackingConsent + self.userInfo = userInfo + self.networkConnectionInfo = networkConnectionInfo + self.carrierInfo = carrierInfo + self.lastIsAppInForeground = lastIsAppInForeground + self.appLaunchDate = appLaunchDate + self.lastRUMViewEvent = lastRUMViewEvent + self.lastRUMSessionState = lastRUMSessionState + self.lastRUMAttributes = lastRUMAttributes + self.lastLogAttributes = lastLogAttributes + } + + init( + _ context: DatadogContext, + lastRUMViewEvent: AnyCodable?, + lastRUMSessionState: AnyCodable?, + lastRUMAttributes: GlobalRUMAttributes?, + lastLogAttributes: AnyCodable? + ) { + self.serverTimeOffset = context.serverTimeOffset + self.service = context.service + self.env = context.env + self.version = context.version + self.buildNumber = context.buildNumber + self.device = context.device + self.sdkVersion = context.sdkVersion + self.source = context.source + self.trackingConsent = context.trackingConsent + self.userInfo = context.userInfo + self.networkConnectionInfo = context.networkConnectionInfo + self.carrierInfo = context.carrierInfo + self.lastIsAppInForeground = context.applicationStateHistory.currentSnapshot.state.isRunningInForeground + + self.lastRUMViewEvent = lastRUMViewEvent + self.lastRUMSessionState = lastRUMSessionState + self.lastRUMAttributes = lastRUMAttributes + self.lastLogAttributes = lastLogAttributes + + self.appLaunchDate = context.launchTime?.launchDate + } + + static func == (lhs: CrashContext, rhs: CrashContext) -> Bool { + lhs.serverTimeOffset == rhs.serverTimeOffset && + lhs.service == rhs.service && + lhs.env == rhs.env && + lhs.version == rhs.version && + lhs.buildNumber == rhs.buildNumber && + lhs.source == rhs.source && + lhs.trackingConsent == rhs.trackingConsent && + lhs.networkConnectionInfo == rhs.networkConnectionInfo && + lhs.carrierInfo == rhs.carrierInfo && + lhs.lastIsAppInForeground == rhs.lastIsAppInForeground && + lhs.userInfo?.id == rhs.userInfo?.id && + lhs.userInfo?.name == rhs.userInfo?.name && + lhs.userInfo?.email == rhs.userInfo?.email && + lhs.appLaunchDate == rhs.appLaunchDate + } +} diff --git a/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift b/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift new file mode 100644 index 0000000000..6d71594ec5 --- /dev/null +++ b/DatadogCrashReporting/Sources/CrashContext/CrashContextProvider.swift @@ -0,0 +1,192 @@ +/* + * 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 + +/// An interface for writing and reading the `CrashContext` +internal protocol CrashContextProvider: AnyObject { + /// Returns current `CrashContext` value. + var currentCrashContext: CrashContext? { get } + /// Notifies on `CrashContext` change. + var onCrashContextChange: (CrashContext) -> Void { set get } +} + +/// Manages the `CrashContext` reads and writes in a thread-safe manner. +internal class CrashContextCoreProvider: CrashContextProvider { + /// Queue for synchronizing `unsafeCrashContext` updates. + private let queue = DispatchQueue( + label: "com.datadoghq.crash-context", + target: .global(qos: .utility) + ) + + /// Unsafe callback instance. + private var _callback: (CrashContext) -> Void = { _ in } + + /// Unsychronized `CrashContext`. The `queue` must be used to synchronize its mutation. + private var _context: CrashContext? { + didSet { _context.map(_callback) } + } + + private var viewEvent: AnyCodable? { + didSet { _context?.lastRUMViewEvent = viewEvent } + } + + private var sessionState: AnyCodable? { + didSet { _context?.lastRUMSessionState = sessionState } + } + + private var logAttributes: AnyCodable? { + didSet { _context?.lastLogAttributes = logAttributes } + } + + private var rumAttributes: GlobalRUMAttributes? { + didSet { _context?.lastRUMAttributes = rumAttributes } + } + + // MARK: - CrashContextProviderType + + var currentCrashContext: CrashContext? { + queue.sync { _context } + } + + var onCrashContextChange: (CrashContext) -> Void { + get { queue.sync { self._callback } } + set { queue.async { self._callback = newValue } } + } +} + +extension CrashContextCoreProvider: FeatureMessageReceiver { + /// Defines keys referencing RUM baggage in `DatadogContext.featuresAttributes`. + internal enum RUMBaggageKeys { + /// The key references RUM view event. + /// The view event associated with the key conforms to `Codable`. + static let viewEvent = "rum-view-event" + + /// The key references a `true` value if the RUM view is reset. + static let viewReset = "rum-view-reset" + + /// The key references RUM session state. + /// The state associated with the key conforms to `Codable`. + static let sessionState = "rum-session-state" + + /// This key references the global log attributes + static let logAttributes = "global-log-attributes" + + /// The key referencing ``DatadogInternal.GlobalRUMAttributes`` value holding RUM global attributes. + static let rumAttributes = "global-rum-attributes" + } + + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + switch message { + case .context(let context): + update(context: context) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.viewEvent: + updateRUMView(with: baggage, to: core) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.viewReset: + resetRUMView(with: baggage, to: core) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.sessionState: + updateSessionState(with: baggage, to: core) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.logAttributes: + updateLogAttributes(with: baggage, to: core) + case .baggage(let label, let baggage) where label == RUMBaggageKeys.rumAttributes: + updateRUMAttributes(with: baggage, to: core) + default: + return false + } + + return true + } + + /// Updates crash context. + /// + /// - Parameter context: The updated core context. + private func update(context: DatadogContext) { + queue.async { [weak self] in + guard let self = self else { + return + } + + let crashContext = CrashContext( + context, + lastRUMViewEvent: self.viewEvent, + lastRUMSessionState: self.sessionState, + lastRUMAttributes: self.rumAttributes, + lastLogAttributes: self.logAttributes + ) + + if crashContext != self._context { + self._context = crashContext + } + } + } + + private func updateRUMView(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in + do { + self?.viewEvent = try baggage.decode(type: AnyCodable.self) + } catch { + core?.telemetry + .error("Fails to decode RUM view event from Crash Reporting", error: error) + } + } + } + + private func resetRUMView(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in + do { + if try baggage.decode(type: Bool.self) { + self?.viewEvent = nil + } + } catch { + core?.telemetry + .error("Fails to decode RUM view reset from Crash Reporting", error: error) + } + } + } + + private func updateSessionState(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in + do { + self?.sessionState = try baggage.decode(type: AnyCodable.self) + } catch { + core?.telemetry + .error("Fails to decode RUM session state from Crash Reporting", error: error) + } + } + } + + private func updateLogAttributes(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in + do { + self?.logAttributes = try baggage.decode(type: AnyCodable.self) + } catch { + core?.telemetry + .error("Fails to decode log attributes from Crash Reporting", error: error) + } + } + } + + private func updateRUMAttributes(with baggage: FeatureBaggage, to core: DatadogCoreProtocol) { + queue.async { [weak core, weak self] in + do { + self?.rumAttributes = try baggage.decode(type: GlobalRUMAttributes.self) + } catch { + core?.telemetry + .error("Fails to decode log attributes from Crash Reporting", error: error) + } + } + } +} + +extension CrashContextCoreProvider: Flushable { + /// Awaits completion of all asynchronous operations. + /// + /// **blocks the caller thread** + func flush() { + queue.sync { } + } +} diff --git a/DatadogCrashReporting/Sources/CrashReporting.swift b/DatadogCrashReporting/Sources/CrashReporting.swift new file mode 100644 index 0000000000..a1dd878a07 --- /dev/null +++ b/DatadogCrashReporting/Sources/CrashReporting.swift @@ -0,0 +1,81 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Enable iOS Crash Reporting and Error Tracking to get comprehensive crash reports and +/// error trends with Real User Monitoring. With this feature, you can access: +/// +/// - Aggregated iOS crash dashboards and attributes +/// - Symbolicated iOS crash reports +/// - Trend analysis with iOS error tracking +/// +/// In order to symbolicate your stack traces, find and upload your .dSYM files to Datadog. +/// Then, verify your configuration by running a test crash and restarting your application. +/// +/// Your crash reports appear in [Error Tracking](https://app.datadoghq.com/rum/error-tracking). +public final class CrashReporting { + /// Initializes the Datadog Crash Reporter using the default + /// `PLCrashReporter` plugin. + public static func enable(in core: DatadogCoreProtocol = CoreRegistry.default) { + enable(with: PLCrashReporterPlugin(), in: core) + } + + /// 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() + + let reporter = CrashReportingFeature( + crashReportingPlugin: plugin, + crashContextProvider: contextProvider, + sender: MessageBusSender(core: core), + messageReceiver: contextProvider, + telemetry: core.telemetry + ) + + try core.register(feature: reporter) + + if let backtraceReporter = plugin.backtraceReporter { + try core.register(backtraceReporter: backtraceReporter) + } + + reporter.sendCrashReportIfFound() + + core.telemetry + .configuration(trackErrors: true) + } catch { + consolePrint("\(error)", .error) + } + } +} + +/// Enable iOS Crash Reporting and Error Tracking to get comprehensive crash reports and +/// error trends with Real User Monitoring. With this feature, you can access: +/// +/// - Aggregated iOS crash dashboards and attributes +/// - Symbolicated iOS crash reports +/// - Trend analysis with iOS error tracking +/// +/// In order to symbolicate your stack traces, find and upload your .dSYM files to Datadog. +/// Then, verify your configuration by running a test crash and restarting your application. +/// +/// Your crash reports appear in [Error Tracking](https://app.datadoghq.com/rum/error-tracking). +@available(swift, obsoleted: 1) +@objc(DDCrashReporter) +public final class objc_CrashReporting: NSObject { + /// Initializes the Datadog Crash Reporter. + @objc + public static func enable() { + CrashReporting.enable() + } +} diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift new file mode 100644 index 0000000000..f716cb401a --- /dev/null +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -0,0 +1,155 @@ +/* + * 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 + +internal final class CrashReportingFeature: DatadogFeature { + static let name = "crash-reporter" + + let messageReceiver: FeatureMessageReceiver + + /// Queue for synchronizing internal operations. + private let queue: DispatchQueue + + let crashContextProvider: CrashContextProvider + + /// An interface for accessing the `DDCrashReportingPlugin` from `DatadogCrashReporting`. + let plugin: CrashReportingPlugin + /// Integration enabling sending crash reports as Logs or RUM Errors. + let sender: CrashReportSender + /// Telemetry interface. + let telemetry: Telemetry + + init( + crashReportingPlugin: CrashReportingPlugin, + crashContextProvider: CrashContextProvider, + sender: CrashReportSender, + messageReceiver: FeatureMessageReceiver, + telemetry: Telemetry + ) { + self.queue = DispatchQueue( + label: "com.datadoghq.crash-reporter", + target: .global(qos: .utility) + ) + self.plugin = crashReportingPlugin + self.sender = sender + self.crashContextProvider = crashContextProvider + self.messageReceiver = messageReceiver + self.telemetry = telemetry + + // Inject current `CrashContext` + if let context = crashContextProvider.currentCrashContext { + inject(currentCrashContext: context) + } + + // Register for future `CrashContext` changes + self.crashContextProvider.onCrashContextChange = { [weak self] in + self?.inject(currentCrashContext: $0) + } + } + + // MARK: - Interaction with `DatadogCrashReporting` plugin + + func sendCrashReportIfFound() { + queue.async { + self.plugin.readPendingCrashReport { [weak self] crashReport in + 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 + } + + DD.logger.debug("Loaded pending crash report") + + 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 + } + } + } + + private func inject(currentCrashContext: CrashContext) { + queue.async { + if let crashContextData = self.encode(crashContext: currentCrashContext) { + self.plugin.inject(context: crashContextData) + } + } + } + + // MARK: - CrashContext Encoding and Decoding + + /// JSON encoder used for writing `CrashContext` into JSON `Data` injected to crash report. + /// Note: this `JSONEncoder` must have the same configuration as the `JSONEncoder` used later for writing payloads to uploadable files. + /// Otherwise the format of data read and uploaded from crash report context will be different than the format of data retrieved from the user + /// and written directly to uploadable file. + internal static let crashContextEncoder: JSONEncoder = .dd.default() + /// JSON decoder used for reading `CrashContext` from JSON `Data` injected to crash report. + /// Note: it must follow a configuration that enables reading data encoded with `crashContextEncoder`. + internal static let crashContextDecoder: JSONDecoder = { + var decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + guard let date = iso8601DateFormatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: Requires ISO8601.") + } + return date + } + return decoder + }() + + private func encode(crashContext: CrashContext) -> Data? { + do { + return try CrashReportingFeature.crashContextEncoder.encode(crashContext) + } catch { + DD.logger.error( + """ + Failed to encode crash report context. The app state information associated with eventual crash + report may be not in sync with the current state of the application. + """, + error: error + ) + + telemetry.error("Failed to encode crash report context", error: error) + return nil + } + } + + private func decode(crashContextData: Data) -> CrashContext? { + do { + return try CrashReportingFeature.crashContextDecoder.decode(CrashContext.self, from: crashContextData) + } catch { + DD.logger.error( + """ + Failed to decode crash report context. The app state information associated with the crash + report won't be in sync with the state of the application when it crashed. + """, + error: error + ) + telemetry.error("Failed to decode crash report context", error: error) + return nil + } + } +} + +extension CrashReportingFeature: Flushable { + func flush() { + // Await asynchronous operations completion to safely sink all pending tasks. + queue.sync {} + } +} diff --git a/DatadogCrashReporting/Sources/CrashReportingPlugin.swift b/DatadogCrashReporting/Sources/CrashReportingPlugin.swift new file mode 100644 index 0000000000..a6a3fd44bc --- /dev/null +++ b/DatadogCrashReporting/Sources/CrashReportingPlugin.swift @@ -0,0 +1,32 @@ +/* + * 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 + +/// An interface for enabling crash reporting feature in Datadog SDK. +/// +/// The SDK calls each API on a background thread and succeeding calls are synchronized. +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`) + /// or something went wrong (`false)`. Depending on the returned value, the crash report will be purged or perserved for future read. + /// + /// The SDK calls this method on a background thread. The implementation is free to choice any thread + /// for executing the `completion`. + 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`. + /// + /// 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/BacktraceReporter.swift b/DatadogCrashReporting/Sources/Integrations/BacktraceReporter.swift new file mode 100644 index 0000000000..29305b0eb1 --- /dev/null +++ b/DatadogCrashReporting/Sources/Integrations/BacktraceReporter.swift @@ -0,0 +1,15 @@ +/* + * 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 DatadogInternal + +internal struct BacktraceReporter: DatadogInternal.BacktraceReporting { + let reporter: ThirdPartyCrashReporter + + func generateBacktrace(threadID: ThreadID) throws -> DatadogInternal.BacktraceReport? { + return try reporter.generateBacktrace(threadID: threadID) + } +} diff --git a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift new file mode 100644 index 0000000000..f9f15a2640 --- /dev/null +++ b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift @@ -0,0 +1,83 @@ +/* + * 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 DatadogInternal + +/// An object for sending crash reports. +internal protocol CrashReportSender { + /// Send the crash report and context to integrations. + /// + /// - Parameters: + /// - 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. +internal struct MessageBusSender: CrashReportSender { + /// Defines keys referencing Crash Report message on the bus. + internal enum MessageKeys { + /// The key for a crash message. + /// + /// Use this key when the crash should be reported + /// as a RUM and a Logs event. + static let crash = "crash" + } + + struct Crash: Encodable { + /// The crash report. + let report: DDCrashReport + /// The crash context + let context: CrashContext + } + + /// The core for sending crash report and context. + /// + /// It must be a weak reference to avoid retain cycle (the `CrashReportSender` is held by crash reporting + /// integration kept by core). + weak var core: DatadogCoreProtocol? + + /// Send the crash report et context on the bus of the core. + /// + /// - Parameters: + /// - report: The crash report. + /// - context: The crash context + func send(report: DDCrashReport, with context: CrashContext) { + guard let core = core, context.trackingConsent == .granted else { + DD.logger.debug("Skipped sending Crash Report as it was recorded with \(context.trackingConsent) consent") + return + } + + core.send( + message: .baggage( + key: MessageKeys.crash, + value: Crash(report: report, context: context) + ), + else: { + DD.logger.warn( + """ + In order to use Crash Reporting, RUM or Logging feature must be enabled. + Make sure `RUM` or `Logs` are enabled when initializing Datadog SDK. + """ + ) + } + ) + } + + /// 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/CrashReport.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/CrashReport.swift new file mode 100644 index 0000000000..846252c211 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/CrashReport.swift @@ -0,0 +1,320 @@ +/* + * 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 CrashReporter + +/// An intermediate representation of crash report when transforming `PLCrashReport` to `DDCrashReport`. +/// +/// It implements preliminary consistency check for Objective-C `PLCrashReport` and provides additional +/// type-safety for accessing its implicitly unwrapped optional values. +internal struct CrashReport { + /// A client-generated 16-byte UUID of the incident. + var incidentIdentifier: String? + /// System information from the moment of crash. + var systemInfo: SystemInfo? + /// Information about the process that crashed. + var processInfo: CrashedProcessInfo? + /// Information about the fatal signal. + var signalInfo: SignalInfo? + /// Uncaught exception information. Only available if a crash was caused by an uncaught exception, otherwise `nil`. + var exceptionInfo: ExceptionInfo? + /// Information about all threads running at the moment of crash. + var threads: [ThreadInfo] + /// Information about binary images loaded by the process. + var binaryImages: [BinaryImageInfo] + /// Custom user data injected before the crash occurred. + var contextData: Data? + /// Additional flag (for telemetry) meaning if any of the stack traces was truncated due to minification. + var wasTruncated: Bool +} + +/// Intermediate representation of `PLCrashReportSystemInfo`. +internal struct SystemInfo { + /// Date and time when the crash report was generated. + var timestamp: Date? +} + +/// Intermediate representation of `PLCrashReportProcessInfo`. +internal struct CrashedProcessInfo { + /// The name of the process. + var processName: String? + /// The process ID. + var processID: UInt + /// The path to the process executable. + var processPath: String? + /// The parent process ID. + var parentProcessID: UInt + /// The parent process name + var parentProcessName: String? +} + +/// Intermediate representation of `PLCrashReportSignalInfo`. +internal struct SignalInfo { + /// The name of the corresponding BSD termination signal, e.g. `SIGTRAP`. + /// This corresponds to _"Exception Type"_ in Apple Crash Report format. + var name: String? + /// Termination signal code, e.g. `"#0`. Together with `address` it can be used for giving more + /// context, e.g. `"#0 at 0x1b0ad6aa8"`. + /// This corresponds to _"Exception Codes"_ in Apple Crash Report format. + var code: String? + /// The faulting instruction address. + var address: UInt64 +} + +/// Intermediate representation of `PLCrashReportExceptionInfo`. +internal struct ExceptionInfo { + /// The exception name, e.g. `NSInternalInconsistencyException`. + var name: String? + /// The exception reason, e.g. `unable to dequeue a cell with identifier foo - (...)`. + var reason: String? + /// The stack trace of this exception. + var stackFrames: [StackFrame] +} + +/// Intermediate representation of `PLCrashReportThreadInfo`. +internal struct ThreadInfo { + /// Application thread number. + var threadNumber: Int + /// If this thread crashed. + var crashed: Bool + /// The stack trace of this thread. + var stackFrames: [StackFrame] +} + +/// Intermediate representation of `PLCrashReportBinaryImageInfo`. +internal struct BinaryImageInfo { + internal struct CodeType { + /// The name of CPU architecture. + var architectureName: String? + } + + /// The UUID of this image. + var uuid: String? + /// The name of this image (referenced by "library name" in the stack frame). + var imageName: String + /// If its a system library image. + var isSystemImage: Bool + /// Image code type (code architecture information). + var codeType: CodeType? + /// The load address of this image. + var imageBaseAddress: UInt64 + /// The size of this image segment. + var imageSize: UInt64 +} + +/// Intermediate representation of `PLCrashReportStackFrameInfo`. +internal struct StackFrame { + /// The number of this frame in the stack trace. + /// This must be recorded as less meaningful stack frames might be removed when minifying the `CrashReport`. + var number: Int + /// The name of the library that produced this frame (the "image name" from binary image). + var libraryName: String? + /// The load address of the library that produced this frame (the "image base address" from binary image). + var libraryBaseAddress: UInt64? + /// The instruction pointer of this frame. + var instructionPointer: UInt64 +} + +// MARK: - Reading intermediate values from PLCR types + +internal struct CrashReportException: Error { + let description: String +} + +extension CrashReport { + init(from plcr: PLCrashReport) throws { + guard let threads = plcr.threads, + let images = plcr.images else { + // Sanity check - this shouldn't be reachable. + // The crash report must specify some threads and some binary images. + throw CrashReportException( + description: "Received inconsistent `PLCrashReport` # has threads = \(plcr.threads != nil), has images = \(plcr.images != nil)" + ) + } + + if let uuid = plcr.uuidRef, let uuidString = CFUUIDCreateString(nil, uuid) { + self.incidentIdentifier = uuidString as String + } else { + self.incidentIdentifier = nil + } + + self.systemInfo = SystemInfo(from: plcr) + self.processInfo = CrashedProcessInfo(from: plcr) + self.signalInfo = SignalInfo(from: plcr) + self.exceptionInfo = ExceptionInfo(from: plcr) + + self.threads = threads + .compactMap { $0 as? PLCrashReportThreadInfo } + .map { ThreadInfo(from: $0, in: plcr) } + + self.binaryImages = images + .compactMap { $0 as? PLCrashReportBinaryImageInfo } + .compactMap { BinaryImageInfo(from: $0) } + + self.contextData = plcr.customData + self.wasTruncated = false + } +} + +extension SystemInfo { + init?(from plcr: PLCrashReport) { + guard let systemInfo = plcr.systemInfo else { + return nil + } + + self.timestamp = systemInfo.timestamp + } +} + +extension CrashedProcessInfo { + init?(from plcr: PLCrashReport) { + guard plcr.hasProcessInfo, let processInfo = plcr.processInfo else { + return nil + } + + self.processName = processInfo.processName + self.processID = processInfo.processID + self.processPath = processInfo.processPath + self.parentProcessID = processInfo.parentProcessID + self.parentProcessName = processInfo.parentProcessName + } +} + +extension SignalInfo { + init?(from plcr: PLCrashReport) { + guard let signalInfo = plcr.signalInfo else { + return nil + } + + self.name = signalInfo.name + self.code = signalInfo.code + self.address = signalInfo.address + } +} + +extension ExceptionInfo { + init?(from plcr: PLCrashReport) { + guard plcr.hasExceptionInfo, let exceptionInfo = plcr.exceptionInfo else { + // The crash was not caused by an uncaught exception. + return nil + } + + self.name = exceptionInfo.exceptionName + self.reason = exceptionInfo.exceptionReason + + if let stackFrames = exceptionInfo.stackFrames { + self.stackFrames = stackFrames + .compactMap { $0 as? PLCrashReportStackFrameInfo } + .enumerated() + .map { number, frame in StackFrame(from: frame, number: number, in: plcr) } + } else { + self.stackFrames = [] + } + } +} + +extension ThreadInfo { + init(from threadInfo: PLCrashReportThreadInfo, in crashReport: PLCrashReport) { + self.threadNumber = threadInfo.threadNumber + self.crashed = threadInfo.crashed + + if let stackFrames = threadInfo.stackFrames { + self.stackFrames = stackFrames + .compactMap { $0 as? PLCrashReportStackFrameInfo } + .enumerated() + .map { number, frame in StackFrame(from: frame, number: number, in: crashReport) } + } else { + self.stackFrames = [] + } + } +} + +extension BinaryImageInfo { + init?(from imageInfo: PLCrashReportBinaryImageInfo) { + guard let imagePath = imageInfo.imageName else { + // We can drop this image as it won't be useful for symbolication + return nil + } + + self.uuid = imageInfo.imageUUID + self.imageName = URL(fileURLWithPath: imagePath).lastPathComponent + + #if targetEnvironment(simulator) + self.isSystemImage = Self.isPathSystemImageInSimulator(imagePath) + #else + self.isSystemImage = Self.isPathSystemImageInDevice(imagePath) + #endif + + if let codeType = imageInfo.codeType { + self.codeType = CodeType(from: codeType) + } else { + // The architecture name of this image is unknown, but symbolication will be possible. + self.codeType = nil + } + + self.imageBaseAddress = imageInfo.imageBaseAddress + self.imageSize = imageInfo.imageSize + } + + static func isPathSystemImageInSimulator(_ path: String) -> Bool { + // in simulator, example system image path: ~/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/... + return path.contains("/Contents/Developer/Platforms/") + } + + static func isPathSystemImageInDevice(_ path: String) -> Bool { + // in device, example user image path: .../containers/Bundle/Application/0000/Runner.app/Frameworks/... + let isUserImage = path.contains("/Bundle/Application/") + return !isUserImage + } +} + +extension BinaryImageInfo.CodeType { + init?(from processorInfo: PLCrashReportProcessorInfo) { + guard processorInfo.typeEncoding == PLCrashReportProcessorTypeEncodingMach else { + // Unknown processor type - skip. + return nil + } + + let type = processorInfo.type + let subtype = processorInfo.subtype + let subtypeMask = UInt64(CPU_SUBTYPE_MASK) + + // Ref. for this check: + // https://github.com/microsoft/plcrashreporter/blob/dbb05c0bc883bde1cfcad83e7add25862c95d11f/Source/PLCrashReportTextFormatter.m#L371 + switch type { + case UInt64(CPU_TYPE_X86): self.architectureName = "i386" + case UInt64(CPU_TYPE_X86_64): self.architectureName = "x86_64" + case UInt64(CPU_TYPE_ARM): self.architectureName = "arm" + case UInt64(CPU_TYPE_ARM64): + switch subtype & ~subtypeMask { + case UInt64(CPU_SUBTYPE_ARM64_ALL): self.architectureName = "arm64" + case UInt64(CPU_SUBTYPE_ARM64_V8): self.architectureName = "armv8" + case UInt64(CPU_SUBTYPE_ARM64E): self.architectureName = "arm64e" + default: self.architectureName = "arm64-unknown" + } + default: + self.architectureName = nil + } + } +} + +extension StackFrame { + init(from stackFrame: PLCrashReportStackFrameInfo, number: Int, in crashReport: PLCrashReport) { + self.number = number + self.instructionPointer = stackFrame.instructionPointer + + // Without "library name" and its "base address" symbolication will not be possible, + // but the presence of this frame in the stack will be still relevant. + let image = crashReport.image(forAddress: stackFrame.instructionPointer) + + self.libraryBaseAddress = image?.imageBaseAddress + + if let imagePath = image?.imageName { + self.libraryName = URL(fileURLWithPath: imagePath).lastPathComponent + } + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/CrashReportMinifier.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/CrashReportMinifier.swift new file mode 100644 index 0000000000..824d5451e4 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/CrashReportMinifier.swift @@ -0,0 +1,94 @@ +/* + * 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 + +/// Reduces information in intermediate `CrashReport`: +/// - it removes binary images which are not necessary for symbolication, +/// - it removes less important stack frames from stack frames which exceed our limits. +internal struct CrashReportMinifier { + struct Constants { + /// The maximum number of stack frames in each stack trace. + /// When stack trace exceeds this limit, it will be reduced by dropping less important frames. + static let maxNumberOfStackFrames = 200 + } + + /// The maximum number of stack frames in each stack trace. + let stackFramesLimit: Int + + init(stackFramesLimit: Int = Constants.maxNumberOfStackFrames) { + self.stackFramesLimit = stackFramesLimit + } + + func minify(crashReport: inout CrashReport) { + var ifAnyStackFrameWasRemoved = false + + // Keep exception stack trace under limit: + if let exceptionStackFrames = crashReport.exceptionInfo?.stackFrames { + let reducedStackFrames = limit(stackFrames: exceptionStackFrames) + ifAnyStackFrameWasRemoved = ifAnyStackFrameWasRemoved || (reducedStackFrames.count != exceptionStackFrames.count) + crashReport.exceptionInfo?.stackFrames = reducedStackFrames + } + + // Keep thread stack traces under limit: + crashReport.threads = crashReport.threads.map { thread in + var thread = thread + let reducedStackFrames = limit(stackFrames: thread.stackFrames) + ifAnyStackFrameWasRemoved = ifAnyStackFrameWasRemoved || (reducedStackFrames.count != thread.stackFrames.count) + thread.stackFrames = reducedStackFrames + return thread + } + + // Set telemetry flag: + crashReport.wasTruncated = ifAnyStackFrameWasRemoved + + // Remove binary images which are not referenced in any stack trace: + crashReport.binaryImages = remove( + binaryImages: crashReport.binaryImages, + notUsedInAnyStackOf: crashReport + ) + } + + // MARK: - Private + + /// Removes less important stack frames to ensure that their count is equal or below `stackFramesLimit`. + /// Frames are removed at the middle of stack trace, which preserves the most important upper and bottom frames. + private func limit(stackFrames: [StackFrame]) -> [StackFrame] { + if stackFrames.count > stackFramesLimit { + var frames = stackFrames + + let numberOfFramesToRemove = stackFrames.count - stackFramesLimit + let middleFrameIndex = stackFrames.count / 2 + let lowerBound = middleFrameIndex - numberOfFramesToRemove / 2 + let upperBound = lowerBound + numberOfFramesToRemove + + frames.removeSubrange(lowerBound.. [BinaryImageInfo] { + var imageNamesFromStackFrames: Set = [] + + // Add image names from exception stack + if let exceptionStackFrames = crashReport.exceptionInfo?.stackFrames { + imageNamesFromStackFrames.formUnion(exceptionStackFrames.compactMap { $0.libraryName }) + } + + // Add image names from thread stacks + crashReport.threads.forEach { thread in + imageNamesFromStackFrames.formUnion(thread.stackFrames.compactMap { $0.libraryName }) + } + + return binaryImages.filter { image in + return imageNamesFromStackFrames.contains(image.imageName) // if it's referenced in the stack trace + } + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportBuilder.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportBuilder.swift new file mode 100644 index 0000000000..52c331a163 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportBuilder.swift @@ -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 DatadogInternal +import CrashReporter + +/// Builds `DDCrashReport` from `PLCrashReport`. +internal struct DDCrashReportBuilder { + private let minifier = CrashReportMinifier() + private let exporter = DDCrashReportExporter() + + func createDDCrashReport(from plCrashReport: PLCrashReport) throws -> DDCrashReport { + // Read intermediate report: + var crashReport = try CrashReport(from: plCrashReport) + + // Minify intermediate report: + minifier.minify(crashReport: &crashReport) + + // Export DDCrashReport: + return exporter.export(crashReport) + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift new file mode 100644 index 0000000000..584867807b --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/DDCrashReportExporter.swift @@ -0,0 +1,226 @@ +/* + * 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 DatadogInternal + +/// Exports intermediate `CrashReport` to `DDCrashReport`. +/// +/// The responsibility of this component is to format crash information for integration with Error Tracking, namely: +/// * `error.type`, +/// * `error.message`, +/// * `error.stack`. +/// +/// Next to the `error` information it exports thread stack frames and binary images for symbolication process. All stack traces +/// are formatted using Apple-like format. The implementation is based on the PLCR's formatter, ref.: +/// https://github.com/microsoft/plcrashreporter/blob/master/Source/PLCrashReportTextFormatter.m +internal struct DDCrashReportExporter { + private let unknown = "" + private let unavailable = "???" + + /// Different signals and their descriptions available in OS. + private let knownSignalDescriptionByName = [ + "SIGSIGNAL 0": "Signal 0", + "SIGHUP": "Hangup", + "SIGINT": "Interrupt", + "SIGQUIT": "Quit", + "SIGILL": "Illegal instruction", + "SIGTRAP": "Trace/BPT trap", + "SIGABRT": "Abort trap", + "SIGEMT": "EMT trap", + "SIGFPE": "Floating point exception", + "SIGKILL": "Killed", + "SIGBUS": "Bus error", + "SIGSEGV": "Segmentation fault", + "SIGSYS": "Bad system call", + "SIGPIPE": "Broken pipe", + "SIGALRM": "Alarm clock", + "SIGTERM": "Terminated", + "SIGURG": "Urgent I/O condition", + "SIGSTOP": "Suspended (signal)", + "SIGTSTP": "Suspended", + "SIGCONT": "Continued", + "SIGCHLD": "Child exited", + "SIGTTIN": "Stopped (tty input)", + "SIGTTOU": "Stopped (tty output)", + "SIGIO": "I/O possible", + "SIGXCPU": "Cputime limit exceeded", + "SIGXFSZ": "Filesize limit exceeded", + "SIGVTALRM": "Virtual timer expired", + "SIGPROF": "Profiling timer expired", + "SIGWINCH": "Window size changes", + "SIGINFO": "Information request", + "SIGUSR1": "User defined signal 1", + "SIGUSR2": "User defined signal 2", + ] + + func export(_ crashReport: CrashReport) -> DDCrashReport { + return DDCrashReport( + date: crashReport.systemInfo?.timestamp, + type: formattedType(for: crashReport), + message: formattedMessage(for: crashReport), + stack: formattedStack(for: crashReport), + threads: formattedThreads(from: crashReport), + binaryImages: formattedBinaryImages(from: crashReport), + meta: formattedMeta(for: crashReport), + wasTruncated: crashReport.wasTruncated, + context: crashReport.contextData, + additionalAttributes: nil + ) + } + + // MARK: - Formatting `error.type`, `error.message` and `error.stack` + + /// Formats the error type - in Datadog Error Tracking this corresponds to `error.type`. + /// + /// **Note:** This value is used for building error's fingerprint in Error Tracking, thus its cardinality must be controlled. + private func formattedType(for crashReport: CrashReport) -> String { + return "\(crashReport.signalInfo?.name ?? unknown) (\(crashReport.signalInfo?.code ?? unknown))" + } + + /// Formats the error message - in Datadog Error Tracking this corresponds to `error.message`. + /// + /// **Note:** This value is used for building error's fingerprint in Error Tracking, thus its cardinality must be controlled. + private func formattedMessage(for crashReport: CrashReport) -> String { + if let exception = crashReport.exceptionInfo { + // If the crash was caused by an uncaught exception + let exceptionName = exception.name ?? unknown // e.g. `NSInvalidArgumentException` + let exceptionReason = exception.reason ?? unknown // e.g. `-[NSObject objectForKey:]: unrecognized selector sent to instance 0x...` + return "Terminating app due to uncaught exception '\(exceptionName)', reason: '\(exceptionReason)'." + } else { + // Use signal description available in OS + guard let signalName = crashReport.signalInfo?.name else { // e.g. SIGILL + return "Application crash: \(unknown)" + } + + if let signalDescription = knownSignalDescriptionByName[signalName] { + return "Application crash: \(signalName) (\(signalDescription))" + } else { + return "Application crash: \(unknown)" + } + } + } + + /// Formats the error stack - in Datadog Error Tracking this corresponds to `error.stack`. + /// + /// **Note:** This produces unsymbolicated stack trace, which is later symbolicated backend-side and used for building error's fingerprint in Error Tracking. + private func formattedStack(for crashReport: CrashReport) -> String { + let crashedThread = crashReport.threads.first { $0.crashed } + let exception = crashReport.exceptionInfo + + // Consider most meaningful stack trace in this order: + // - uncaught exception stack trace (if available) + // - crashed thread stack trace (must be available) + // - first thread stack trace (sanity fallback) + let mostMeaningfulStackFrames = exception?.stackFrames + ?? crashedThread?.stackFrames + ?? crashReport.threads.first?.stackFrames + + guard let stackFrames = mostMeaningfulStackFrames else { + return unavailable // should never be reached + } + + return string(from: sanitized(stackFrames: stackFrames)) + } + + // MARK: - Sanitizing + + private func sanitized(stackFrames: [StackFrame]) -> [StackFrame] { + guard let lastFrame = stackFrames.last else { + return stackFrames + } + + // RUMM-2025: Often the last frame has no library name nor its base address. This results with + // producing malformed frame, e.g. `XX ??? 0x00000001045f0250 0x000000000 + 4368302672` + // which can't be symbolicated. To make it cleaner in UI and to avoid BE symbolication errors, we filter + // out such trailing frame. Ref.: https://github.com/microsoft/plcrashreporter/issues/193 + let sanitizedFrames = lastFrame.libraryBaseAddress == nil ? stackFrames.dropLast() : stackFrames + return sanitizedFrames + } + + // MARK: - Exporting threads and binary images + + private func formattedThreads(from crashReport: CrashReport) -> [DDThread] { + return crashReport.threads.map { thread in + return DDThread( + name: "Thread \(thread.threadNumber)", + stack: string(from: thread.stackFrames), // we don't sanitize frames in `error.threads[]` + crashed: thread.crashed, + state: nil // TODO: RUMM-1462 Send registers state for crashed thread + ) + } + } + + private func formattedBinaryImages(from crashReport: CrashReport) -> [BinaryImage] { + return crashReport.binaryImages.map { image in + // Ref. for this computation: + // https://github.com/microsoft/plcrashreporter/blob/dbb05c0bc883bde1cfcad83e7add25862c95d11f/Source/PLCrashReportTextFormatter.m#L447 + let loadAddressHex = "0x\(image.imageBaseAddress.toHex)" + var maxAddressOffset = image.imageSize.subtractIfNoOverflow(1) ?? image.imageSize + maxAddressOffset = max(1, maxAddressOffset) + let maxAddress = image.imageBaseAddress.addIfNoOverflow(maxAddressOffset) ?? image.imageBaseAddress + let maxAddressHex = "0x\(maxAddress.toHex)" + + return BinaryImage( + libraryName: image.imageName, + uuid: image.uuid ?? unavailable, + architecture: image.codeType?.architectureName ?? unavailable, + isSystemLibrary: image.isSystemImage, + loadAddress: loadAddressHex, + maxAddress: maxAddressHex + ) + } + } + + // MARK: - Exporting meta information + + private func formattedMeta(for crashReport: CrashReport) -> DDCrashReport.Meta { + let process = crashReport.processInfo.map { info in + info.processName.map { "\($0) [\(info.processID)]" } ?? "[\(info.processID)]" + } + + let parentProcess = crashReport.processInfo.map { info in + info.parentProcessName.map { "\($0) [\(info.parentProcessID)]" } ?? "[\(info.parentProcessID)]" + } + + let anyBinaryImageWithKnownArchitecture = crashReport.binaryImages.first { $0.codeType?.architectureName != nil } + let cpuArchitecture = anyBinaryImageWithKnownArchitecture?.codeType?.architectureName + + return .init( + incidentIdentifier: crashReport.incidentIdentifier, + process: process, + parentProcess: parentProcess, + path: crashReport.processInfo?.processPath, + codeType: cpuArchitecture, + exceptionType: crashReport.signalInfo?.name, + exceptionCodes: crashReport.signalInfo?.code + ) + } + + // MARK: - Common + + /// Converts stack frames to newline-separated text format. + private func string(from stackFrames: [StackFrame]) -> String { + let lines: [String] = stackFrames.map { frame in + let frameNumber = "\(frame.number)".addSuffix(repeating: " ", targetLength: 3) + let libraryName = (frame.libraryName ?? unavailable).addSuffix(repeating: " ", targetLength: 35) + + // Ref. for this computations: + // https://github.com/microsoft/plcrashreporter/blob/dbb05c0bc883bde1cfcad83e7add25862c95d11f/Source/PLCrashReportTextFormatter.m#L496-L499 + let instructionAddressHex = "0x\(frame.instructionPointer.toHex.addPrefix(repeating: "0", targetLength: 16))" + var imageBaseAddressHex = "0x0" + var instructionOffsetDec = "0" + + if let libraryBaseAddress = frame.libraryBaseAddress { + imageBaseAddressHex = "0x\(libraryBaseAddress.toHex)" + instructionOffsetDec = "\(frame.instructionPointer.subtractIfNoOverflow(libraryBaseAddress) ?? 0)" + } + + return "\(frameNumber) \(libraryName) \(instructionAddressHex) \(imageBaseAddressHex) + \(instructionOffsetDec)" + } + + return lines.joined(separator: "\n") + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.swift new file mode 100644 index 0000000000..5ac30bfeb0 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterIntegration.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 Foundation +import DatadogInternal + +@preconcurrency import CrashReporter + +internal extension PLCrashReporterConfig { + /// `PLCR` configuration used for `DatadogCrashReporting` + static func ddConfiguration() throws -> PLCrashReporterConfig { + let version = "v1" + + guard let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + throw CrashReportException(description: "Cannot obtain `/Library/Caches/` url.") + } + + let directory = cache.appendingPathComponent("com.datadoghq.crash-reporting/\(version)", isDirectory: true) + + return PLCrashReporterConfig( + // The choice of `.BSD` over `.mach` is well discussed here: + // https://github.com/microsoft/PLCrashReporter/blob/7f27b272d5ff0d6650fc41317127bb2378ed6e88/Source/CrashReporter.h#L238-L363 + signalHandlerType: .BSD, + // We don't symbolicate on device. All symbolication will happen backend-side. + symbolicationStrategy: [], + // Set a custom path to avoid conflicts with other PLC instances + basePath: directory.path + ) + } +} + +internal final class PLCrashReporterIntegration: ThirdPartyCrashReporter { + private let crashReporter: PLCrashReporter + private let builder = DDCrashReportBuilder() + + init() throws { + self.crashReporter = try PLCrashReporter(configuration: .ddConfiguration()) + try crashReporter.enableAndReturnError() + } + + func hasPendingCrashReport() -> Bool { + return crashReporter.hasPendingCrashReport() + } + + func loadPendingCrashReport() throws -> DDCrashReport { + let crashReportData = try crashReporter.loadPendingCrashReportDataAndReturnError() + let crashReport = try PLCrashReport(data: crashReportData) + let ddCrashReport = try builder.createDDCrashReport(from: crashReport) + return ddCrashReport + } + + func inject(context: Data) { + crashReporter.customData = context + } + + func purgePendingCrashReport() throws { + try crashReporter.purgePendingCrashReportAndReturnError() + } + + func generateBacktrace(threadID: ThreadID) throws -> BacktraceReport { + let liveReportData = crashReporter.generateLiveReport(withThread: threadID) + let liveReport = try PLCrashReport(data: liveReportData) + + // This is quite opportunistic - we map PLCR's live report through existing `DDCrashReport` builder to + // then extract essential elements for assembling `BacktraceReport`. It works for now, but be careful + // with how this evolves. We may need a dedicated `BacktraceReport` builder that only shares some code + // with `DDCrashReport` builder. + let crashReport = try builder.createDDCrashReport(from: liveReport) + return BacktraceReport( + stack: crashReport.stack, + threads: crashReport.threads.map { thread in + var thread = thread + // PLCR sets `crashed` flag for the primary thread in `liveReport`. Because we're not dealing with the crash situation + // we reset this flag accordingly. + thread.crashed = false + return thread + }, + binaryImages: crashReport.binaryImages, + wasTruncated: crashReport.wasTruncated + ) + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.swift new file mode 100644 index 0000000000..a59252fc50 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/PLCrashReporterPlugin.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 Foundation +import DatadogInternal + +/// The implementation of `Datadog.DDCrashReportingPluginType`. +/// Pass its instance as the crash reporting plugin for Datadog SDK to enable crash reporting feature. +@objc +internal class PLCrashReporterPlugin: NSObject, CrashReportingPlugin { + static var thirdPartyCrashReporter: ThirdPartyCrashReporter? + + // MARK: - Initialization + + override convenience init() { + self.init { try PLCrashReporterIntegration() } + } + + internal init(thirdPartyCrashReporterFactory: () throws -> ThirdPartyCrashReporter) { + PLCrashReporterPlugin.enableOnce(using: thirdPartyCrashReporterFactory) + } + + private static func enableOnce(using thirdPartyCrashReporterFactory: () throws -> ThirdPartyCrashReporter) { + if thirdPartyCrashReporter == nil { + do { + thirdPartyCrashReporter = try thirdPartyCrashReporterFactory() + } catch { + consolePrint("🔥 DatadogCrashReporting error: failed to enable crash reporter: \(error)", .error) + } + } + } + + // MARK: - DDCrashReportingPluginType + + func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) { + guard let crashReporter = PLCrashReporterPlugin.thirdPartyCrashReporter, + crashReporter.hasPendingCrashReport() else { + _ = completion(nil) + return + } + + do { + let crashReport = try crashReporter.loadPendingCrashReport() + let wasProcessed = completion(crashReport) + + if wasProcessed { + try? crashReporter.purgePendingCrashReport() + } + } catch { + _ = completion(nil) + consolePrint("🔥 DatadogCrashReporting error: failed to load crash report: \(error)", .error) + } + } + + func inject(context: Data) { + PLCrashReporterPlugin.thirdPartyCrashReporter?.inject(context: context) + } + + var backtraceReporter: BacktraceReporting? { + PLCrashReporterPlugin.thirdPartyCrashReporter.map { BacktraceReporter(reporter: $0) } + } +} diff --git a/DatadogCrashReporting/Sources/PLCrashReporterIntegration/Utils/SwiftExtensions.swift b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/Utils/SwiftExtensions.swift new file mode 100644 index 0000000000..244095ab01 --- /dev/null +++ b/DatadogCrashReporting/Sources/PLCrashReporterIntegration/Utils/SwiftExtensions.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 + +internal extension UInt64 { + /// Returns the difference obtained by subtracting the given value from this value only if + /// it doesn't overflow (otherwise it returns `nil`). + func subtractIfNoOverflow(_ otherUInt64: UInt64) -> UInt64? { + let (partialValue, overflow) = subtractingReportingOverflow(otherUInt64) + return overflow ? nil : partialValue + } + + /// Returns the sum of this value and the given value only if it doesn't overflow (otherwise it returns `nil`). + func addIfNoOverflow(_ otherUInt64: UInt64) -> UInt64? { + let (partialValue, overflow) = addingReportingOverflow(otherUInt64) + return overflow ? nil : partialValue + } + + /// Returns hexadecimal representation of this value. + var toHex: String { String(self, radix: 16, uppercase: false) } +} + +internal extension String { + func addPrefix(repeating character: Character, targetLength: Int) -> String { + let prefix = String(repeating: character, count: max(0, targetLength - count)) + return "\(prefix)\(self)" + } + + func addSuffix(repeating character: Character, targetLength: Int) -> String { + let suffix = String(repeating: character, count: max(0, targetLength - count)) + return "\(self)\(suffix)" + } +} diff --git a/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift new file mode 100644 index 0000000000..8d7109f512 --- /dev/null +++ b/DatadogCrashReporting/Sources/ThirdPartyCrashReporter.swift @@ -0,0 +1,32 @@ +/* + * 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 + +/// An interface of 3rd party crash reporter used by the DatadogCrashReporting. +internal protocol ThirdPartyCrashReporter: Sendable { + /// Initializes and enables the crash reporter. + init() throws + + // MARK: - Crash Reporting + + /// Tells if there is a crash report available. + func hasPendingCrashReport() -> Bool + + /// Loads pending crash report. + func loadPendingCrashReport() throws -> DDCrashReport + + /// Injects custom `context` to the crash reporter so it will be attached to the `DDCrashReport`. + func inject(context: Data) + + /// Deletes the available crash report. + func purgePendingCrashReport() throws + + // MARK: - Backtrace Generation + + func generateBacktrace(threadID: ThreadID) throws -> BacktraceReport +} diff --git a/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift new file mode 100644 index 0000000000..c2535470c7 --- /dev/null +++ b/DatadogCrashReporting/Tests/CrashReportingPluginTests.swift @@ -0,0 +1,153 @@ +/* +* 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 + +@testable import DatadogCrashReporting + +class CrashReportingPluginTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(PLCrashReporterPlugin.thirdPartyCrashReporter) + } + + override func tearDown() { + XCTAssertNil(PLCrashReporterPlugin.thirdPartyCrashReporter) + super.tearDown() + } + + // MARK: - Processing Crash Report in Caller + + func testGivenPendingCrashReport_whenCallerSucceedsWithItsProcessing_itIsPurged() throws { + let expectation = self.expectation(description: "Crash Report was delivered to the caller.") + let crashReporter = try ThirdPartyCrashReporterMock() + let plugin = PLCrashReporterPlugin { crashReporter } + defer { PLCrashReporterPlugin.thirdPartyCrashReporter = nil } + + // Given + crashReporter.pendingCrashReport = .mockAny() + + // When + plugin.readPendingCrashReport { crashReport in + DDAssertReflectionEqual(crashReport, crashReporter.pendingCrashReport) + expectation.fulfill() + return true // the caller succeeded in processing the crash report + } + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertTrue(crashReporter.hasPurgedPendingCrashReport) + } + + func testGivenPendingCrashReport_whenCallerSucceedsInItsProcessing_itIsPurged() throws { + let expectation = self.expectation(description: "Crash Report was delivered to the caller.") + let crashReporter = try ThirdPartyCrashReporterMock() + let plugin = PLCrashReporterPlugin { crashReporter } + defer { PLCrashReporterPlugin.thirdPartyCrashReporter = nil } + + // Given + crashReporter.pendingCrashReport = .mockAny() + + // When + plugin.readPendingCrashReport { crashReport in + DDAssertReflectionEqual(crashReport, crashReporter.pendingCrashReport) + expectation.fulfill() + return true + } + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertTrue(crashReporter.hasPurgedPendingCrashReport) + } + + func testGivenNoPendingCrashReport_whenCallerRequestIsMade_itReceivesNil() throws { + let expectation = self.expectation(description: "No Crash Report was delivered to the caller.") + let crashReporter = try ThirdPartyCrashReporterMock() + let plugin = PLCrashReporterPlugin { crashReporter } + defer { PLCrashReporterPlugin.thirdPartyCrashReporter = nil } + + // Given + crashReporter.pendingCrashReport = nil + + // When + plugin.readPendingCrashReport { crashReport in + XCTAssertNil(crashReport) + expectation.fulfill() + return true + } + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + } + + // MARK: - Injecting Crash Context + + func testItForwardsCrashContextToCrashReporter() throws { + let crashReporter = try ThirdPartyCrashReporterMock() + let plugin = PLCrashReporterPlugin { crashReporter } + defer { PLCrashReporterPlugin.thirdPartyCrashReporter = nil } + + let context = "some context".data(using: .utf8)! + plugin.inject(context: context) + + XCTAssertEqual(crashReporter.injectedContext, context) + } + + // 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.") + + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } + + let crashReporter = try ThirdPartyCrashReporterMock() + let plugin = PLCrashReporterPlugin { crashReporter } + defer { PLCrashReporterPlugin.thirdPartyCrashReporter = nil } + + // Given + crashReporter.pendingCrashReport = .mockAny() + crashReporter.pendingCrashReportError = ErrorMock("Reading error") + + // When + plugin.readPendingCrashReport { crashReport in + XCTAssertNil(crashReport) + expectation.fulfill() + return .random() + } + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertFalse(crashReporter.hasPurgedPendingCrashReport) + XCTAssertEqual( + printFunction.printedMessage, + "🔥 DatadogCrashReporting error: failed to load crash report: Reading error" + ) + } + + func testWhenCrashReporterCannotBeEnabled_itPrintsError() { + let previousPrint = consolePrint + consolePrint = printFunction.print + defer { consolePrint = previousPrint } + + // When + ThirdPartyCrashReporterMock.initializationError = ErrorMock("Initialization error") + defer { ThirdPartyCrashReporterMock.initializationError = nil } + + // Then + _ = PLCrashReporterPlugin { try ThirdPartyCrashReporterMock() } + + XCTAssertEqual( + printFunction.printedMessage, + "🔥 DatadogCrashReporting error: failed to enable crash reporter: Initialization error" + ) + } +} diff --git a/DatadogCrashReporting/Tests/Mocks.swift b/DatadogCrashReporting/Tests/Mocks.swift new file mode 100644 index 0000000000..6a9d33863a --- /dev/null +++ b/DatadogCrashReporting/Tests/Mocks.swift @@ -0,0 +1,271 @@ +/* + * 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 DatadogInternal +import CrashReporter +@testable import DatadogCrashReporting + +internal final class ThirdPartyCrashReporterMock: ThirdPartyCrashReporter, @unchecked Sendable { + static var initializationError: Error? + + var pendingCrashReport: DDCrashReport? + var pendingCrashReportError: Error? + + var injectedContext: Data? + + var hasPurgedPendingCrashReport = false + var hasPurgedPendingCrashReportError: Error? + + var generatedBacktrace: BacktraceReport = .mockAny() + + required init() throws { + if let error = ThirdPartyCrashReporterMock.initializationError { + throw error + } + } + + func hasPendingCrashReport() -> Bool { + return pendingCrashReport != nil + } + + func loadPendingCrashReport() throws -> DDCrashReport { + if let error = pendingCrashReportError { + throw error + } + return pendingCrashReport! + } + + func inject(context: Data) { + injectedContext = context + } + + func purgePendingCrashReport() throws { + if let error = hasPurgedPendingCrashReportError { + throw error + } + hasPurgedPendingCrashReport = true + } + + func generateBacktrace(threadID: ThreadID) throws -> BacktraceReport { + return generatedBacktrace + } +} + +// swiftlint:disable implicitly_unwrapped_optional +internal class PLCrashReportMock: PLCrashReport { + class SystemInfoMock: PLCrashReportSystemInfo { + var mockTimestamp: Date! = nil + + override var timestamp: Date! { mockTimestamp } + } + + class ProcessInfoMock: PLCrashReportProcessInfo { + var mockProcessName: String! = nil + var mockProcessPath: String! = nil + var mockParentProcessID: UInt = 0 + var mockParentProcessName: String! = nil + + override var processName: String! { mockProcessName } + override var processPath: String! { mockProcessPath } + override var parentProcessID: UInt { mockParentProcessID } + override var parentProcessName: String! { mockParentProcessName } + } + + class SignalInfo: PLCrashReportSignalInfo { + var mockName: String! = nil + var mockAddress: UInt64 = 0 + var mockCode: String! = nil + + override var name: String! { mockName } + override var address: UInt64 { mockAddress } + override var code: String! { mockCode } + } + + class StackFrame: PLCrashReportStackFrameInfo { + var mockInstructionPointer: UInt64 = 0 + + override var instructionPointer: UInt64 { mockInstructionPointer } + } + + class ExceptionInfo: PLCrashReportExceptionInfo { + var mockExceptionName: String! = nil + var mockExceptionReason: String! = nil + var mockStackFrames: [StackFrame]! = nil + + override var exceptionName: String! { mockExceptionName } + override var exceptionReason: String! { mockExceptionReason } + override var stackFrames: [Any]! { mockStackFrames } + } + + class ThreadInfo: PLCrashReportThreadInfo { + var mockThreadNumber: Int = 0 + var mockCrashed = false + var mockStackFrames: [StackFrame]! = nil + + override var threadNumber: Int { mockThreadNumber } + override var crashed: Bool { mockCrashed } + override var stackFrames: [Any]! { mockStackFrames } + } + + class BinaryImageInfo: PLCrashReportBinaryImageInfo { + class ProcessorInfo: PLCrashReportProcessorInfo { + var mockTypeEncoding: PLCrashReportProcessorTypeEncoding = PLCrashReportProcessorTypeEncodingUnknown + var mockType: UInt64 = 0 + var mockSubtype: UInt64 = 0 + + override var typeEncoding: PLCrashReportProcessorTypeEncoding { mockTypeEncoding } + override var type: UInt64 { mockType } + override var subtype: UInt64 { mockSubtype } + } + + var mockImageName: String! = nil + var mockHasImageUUID = false + var mockImageUUID: String! = nil + var mockImageSize: UInt64 = 0 + var mockImageBaseAddress: UInt64 = 0 + var mockCodeType: ProcessorInfo! = nil + + override var imageName: String! { mockImageName } + override var hasImageUUID: Bool { mockHasImageUUID } + override var imageUUID: String! { mockImageUUID } + override var imageSize: UInt64 { mockImageSize } + override var imageBaseAddress: UInt64 { mockImageBaseAddress } + override var codeType: PLCrashReportProcessorInfo! { mockCodeType } + } + + var mockSystemInfo: SystemInfoMock! = nil + var mockUUIDRef: CFUUID! = nil + var mockHasProcessInfo = false + var mockProcessInfo: ProcessInfoMock! = nil + var mockSignalInfo: SignalInfo! = nil + var mockHasExceptionInfo = false + var mockExceptionInfo: ExceptionInfo! = nil + var mockThreads: [ThreadInfo]! = nil + var mockImages: [BinaryImageInfo]! = nil + var mockCustomData: Data! = nil + + override var systemInfo: PLCrashReportSystemInfo! { mockSystemInfo } + override var uuidRef: CFUUID! { mockUUIDRef } + override var hasProcessInfo: Bool { mockHasProcessInfo } + override var processInfo: PLCrashReportProcessInfo! { mockProcessInfo } + override var signalInfo: PLCrashReportSignalInfo! { mockSignalInfo } + override var hasExceptionInfo: Bool { mockHasExceptionInfo } + override var exceptionInfo: PLCrashReportExceptionInfo! { mockExceptionInfo } + override var threads: [Any]! { mockThreads } + override var images: [Any]! { mockImages } + override var customData: Data! { mockCustomData } + + var mockImageForAddress: [UInt64: PLCrashReportBinaryImageInfo] = [:] + + override func image(forAddress address: UInt64) -> PLCrashReportBinaryImageInfo! { + return mockImageForAddress[address] + } +} +// swiftlint:enable implicitly_unwrapped_optional + +extension CrashReport { + static func mockAny() -> CrashReport { + return mockWith() + } + + static func mockWith( + incidentIdentifier: String? = nil, + systemInfo: SystemInfo? = nil, + processInfo: CrashedProcessInfo? = nil, + signalInfo: SignalInfo? = nil, + exceptionInfo: ExceptionInfo? = nil, + threads: [ThreadInfo] = [], + binaryImages: [BinaryImageInfo] = [], + contextData: Data? = nil, + wasTruncated: Bool = false + ) -> CrashReport { + return CrashReport( + incidentIdentifier: incidentIdentifier, + systemInfo: systemInfo, + processInfo: processInfo, + signalInfo: signalInfo, + exceptionInfo: exceptionInfo, + threads: threads, + binaryImages: binaryImages, + contextData: contextData, + wasTruncated: wasTruncated + ) + } +} + +extension CrashedProcessInfo { + static func mockWith( + processName: String? = nil, + processID: UInt = 1, + processPath: String? = nil, + parentProcessID: UInt = 0, + parentProcessName: String? = nil + ) -> CrashedProcessInfo { + return CrashedProcessInfo( + processName: processName, + processID: processID, + processPath: processPath, + parentProcessID: parentProcessID, + parentProcessName: parentProcessName + ) + } +} + +extension ExceptionInfo { + static func mockWith( + name: String? = .mockAny(), + reason: String? = .mockAny(), + stackFrames: [StackFrame] = [] + ) -> ExceptionInfo { + return ExceptionInfo(name: name, reason: reason, stackFrames: stackFrames) + } +} + +extension ThreadInfo { + static func mockWith( + threadNumber: Int = .mockAny(), + crashed: Bool = .mockAny(), + stackFrames: [StackFrame] = [] + ) -> ThreadInfo { + return ThreadInfo(threadNumber: threadNumber, crashed: crashed, stackFrames: stackFrames) + } +} + +extension BinaryImageInfo { + static func mockWith( + uuid: String? = .mockAny(), + imageName: String = .mockAny(), + isSystemImage: Bool = .random(), + architectureName: String? = .mockAny(), + imageBaseAddress: UInt64 = .mockAny(), + imageSize: UInt64 = .mockAny() + ) -> BinaryImageInfo { + return BinaryImageInfo( + uuid: uuid, + imageName: imageName, + isSystemImage: isSystemImage, + codeType: .init(architectureName: architectureName), + imageBaseAddress: imageBaseAddress, + imageSize: imageSize + ) + } +} + +extension StackFrame { + static func mockWith( + number: Int = .mockAny(), + libraryName: String? = .mockAny(), + libraryBaseAddress: UInt64? = .mockAny(), + instructionPointer: UInt64 = .mockAny() + ) -> StackFrame { + return StackFrame( + number: number, + libraryName: libraryName, + libraryBaseAddress: libraryBaseAddress, + instructionPointer: instructionPointer + ) + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportMinifierTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportMinifierTests.swift new file mode 100644 index 0000000000..0502f86039 --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportMinifierTests.swift @@ -0,0 +1,197 @@ +/* + * 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 DatadogCrashReporting + +class CrashReportMinifierTests: XCTestCase { + private var crashReport: CrashReport = .mockAny() + + // MARK: - Minimizing number of stack frames + + func tesWhenNumberOfStackFramesExceedsTheLimit_itRemovesFramesToFitTheLimit() { + // Given + let limit: Int = .mockRandom(min: 10, max: 2_048) + let stackFrames: [StackFrame] = (0..<(limit * 2)).map { .mockWith(number: $0) } + + // When + XCTAssertGreaterThan(stackFrames.count, limit) + + crashReport.wasTruncated = false + crashReport.exceptionInfo = .mockWith(stackFrames: stackFrames) + crashReport.threads = (0.. = [] + imageNamesFromStackFrames.formUnion(crashReport.exceptionInfo!.stackFrames.map { $0.libraryName! }) + imageNamesFromStackFrames.formUnion(crashReport.threads.flatMap { $0.stackFrames.map { $0.libraryName! } }) + + var imageNamesFromBinaryImages: Set = [] + imageNamesFromBinaryImages.formUnion(crashReport.binaryImages.map { $0.imageName }) + + XCTAssertEqual( + imageNamesFromStackFrames, + imageNamesFromBinaryImages, + "Reduced `crashReport.binaryImages` should only contain images referenced from stack frames" + ) + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportTests.swift new file mode 100644 index 0000000000..912fcaffc2 --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/CrashReportTests.swift @@ -0,0 +1,379 @@ +/* + * 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 DatadogCrashReporting +import CrashReporter + +class CrashReportTests: XCTestCase { + // MARK: - Consistency + + func testGivenPLCrashReportWithConsistentValues_whenInitializing_itReturnsValue() throws { + // Given + let mockStackFrame = PLCrashReportMock.StackFrame() + + let mockThread = PLCrashReportMock.ThreadInfo() + mockThread.mockStackFrames = [mockStackFrame] + + let mockImage = PLCrashReportMock.BinaryImageInfo() + mockImage.mockHasImageUUID = true + mockImage.mockImageUUID = .mockRandom() + mockImage.mockImageName = .mockRandom() + + let mock = PLCrashReportMock() + mock.mockUUIDRef = CFUUIDCreate(nil) + mock.mockSystemInfo = .init() + mock.mockProcessInfo = .init() + mock.mockHasProcessInfo = true + mock.mockSignalInfo = .init() + mock.mockExceptionInfo = .init() + mock.mockHasExceptionInfo = true + mock.mockThreads = [mockThread] + mock.mockImages = [mockImage] + + // When + let crashReport = try CrashReport(from: mock) + + // Then + XCTAssertNotNil(crashReport.incidentIdentifier) + XCTAssertNotNil(crashReport.systemInfo) + XCTAssertNotNil(crashReport.processInfo) + XCTAssertNotNil(crashReport.signalInfo) + XCTAssertNotNil(crashReport.exceptionInfo) + XCTAssertEqual(crashReport.threads.count, 1) + XCTAssertEqual(crashReport.threads[0].stackFrames.count, 1) + XCTAssertEqual(crashReport.binaryImages.count, 1) + } + + func testGivenPLCrashReportWithNoThreadsAndImages_whenInitializing_itReturnsNil() throws { + // Given + let mock = PLCrashReportMock() + mock.mockThreads = nil + mock.mockImages = nil + + // When + XCTAssertThrowsError(try CrashReport(from: mock)) { error in + // Then + let exception = error as! CrashReportException + XCTAssertEqual(exception.description, "Received inconsistent `PLCrashReport` # has threads = false, has images = false") + } + } + + func testGivenPLCrashReportWithSomeInconsistentValues_whenInitializing_itReturnsValue() throws { + // Given + let mock = PLCrashReportMock() + mock.mockUUIDRef = Bool.random() ? CFUUIDCreate(nil) : nil + mock.mockSystemInfo = Bool.random() ? .init() : nil + mock.mockProcessInfo = Bool.random() ? .init() : nil + mock.mockHasProcessInfo = Bool.random() + mock.mockSignalInfo = Bool.random() ? .init() : nil + mock.mockExceptionInfo = Bool.random() ? .init() : nil + mock.mockHasExceptionInfo = Bool.random() + mock.mockThreads = [.init()] + mock.mockImages = [.init()] + + // When + let crashReport = try CrashReport(from: mock) + + // Then + XCTAssertNotNil(crashReport, "It should initialize as long as it has threads and images") + } + + // MARK: - Values + + func testItReadsIncidentIdentifier() throws { + // Given + let uuid = UUID().uuidString + + let mock = PLCrashReportMock() + mock.mockUUIDRef = CFUUIDCreateFromString(nil, uuid as CFString) + mock.mockThreads = [.init()] + mock.mockImages = [.init()] + + // When + let crashReport = try XCTUnwrap(CrashReport(from: mock)) + + // Then + XCTAssertEqual(crashReport.incidentIdentifier, uuid) + } + + func testItReadsSystemInfo() throws { + // Given + let mock = PLCrashReportMock() + mock.mockSystemInfo = .init() + mock.mockSystemInfo.mockTimestamp = .mockRandomInThePast() + mock.mockThreads = [.init()] + mock.mockImages = [.init()] + + // When + let crashReport = try XCTUnwrap(CrashReport(from: mock)) + + // Then + XCTAssertEqual(crashReport.systemInfo?.timestamp, mock.mockSystemInfo.mockTimestamp) + } + + func testItReadsProcessInfo() throws { + // Given + let mock = PLCrashReportMock() + mock.mockHasProcessInfo = true + mock.mockProcessInfo = .init() + mock.mockProcessInfo.mockProcessName = .mockRandom() + mock.mockProcessInfo.mockProcessPath = .mockRandom() + mock.mockProcessInfo.mockParentProcessID = .mockRandom() + mock.mockProcessInfo.mockParentProcessName = .mockRandom() + mock.mockThreads = [.init()] + mock.mockImages = [.init()] + + // When + let crashReport = try XCTUnwrap(CrashReport(from: mock)) + + // Then + XCTAssertEqual(crashReport.processInfo?.processName, mock.mockProcessInfo.mockProcessName) + XCTAssertEqual(crashReport.processInfo?.processPath, mock.mockProcessInfo.mockProcessPath) + XCTAssertEqual(crashReport.processInfo?.parentProcessID, mock.mockProcessInfo.mockParentProcessID) + XCTAssertEqual(crashReport.processInfo?.parentProcessName, mock.mockProcessInfo.mockParentProcessName) + } + + func testItReadsSignalInfo() throws { + // Given + let mock = PLCrashReportMock() + mock.mockSignalInfo = .init() + mock.mockSignalInfo.mockName = .mockRandom() + mock.mockSignalInfo.mockCode = .mockRandom() + mock.mockSignalInfo.mockAddress = .mockRandom() + mock.mockThreads = [.init()] + mock.mockImages = [.init()] + + // When + let crashReport = try XCTUnwrap(CrashReport(from: mock)) + + // Then + XCTAssertEqual(crashReport.signalInfo?.name, mock.mockSignalInfo.mockName) + XCTAssertEqual(crashReport.signalInfo?.code, mock.mockSignalInfo.mockCode) + XCTAssertEqual(crashReport.signalInfo?.address, mock.mockSignalInfo.mockAddress) + } + + func testItReadsExceptionInfo() throws { + // Given + let mockStackFrames: [PLCrashReportMock.StackFrame] = (0x01..<0x10).map { value in + let mockStackFrame = PLCrashReportMock.StackFrame() + mockStackFrame.mockInstructionPointer = UInt64(value) + return mockStackFrame + } + + let mock = PLCrashReportMock() + mock.mockHasExceptionInfo = true + mock.mockExceptionInfo = .init() + mock.mockExceptionInfo.mockExceptionName = .mockRandom() + mock.mockExceptionInfo.mockExceptionReason = .mockRandom() + mock.mockExceptionInfo.mockStackFrames = mockStackFrames + + // When + let exceptionInfo = try XCTUnwrap(ExceptionInfo(from: mock)) + + // Then + XCTAssertEqual(exceptionInfo.name, mock.mockExceptionInfo.mockExceptionName) + XCTAssertEqual(exceptionInfo.reason, mock.mockExceptionInfo.mockExceptionReason) + XCTAssertEqual(exceptionInfo.stackFrames.count, mockStackFrames.count) + } + + func testItReadsThreadInfo() throws { + // Given + let mockStackFrames: [PLCrashReportMock.StackFrame] = (0x01..<0x10).map { value in + let mockStackFrame = PLCrashReportMock.StackFrame() + mockStackFrame.mockInstructionPointer = UInt64(value) + return mockStackFrame + } + + let mockThread = PLCrashReportMock.ThreadInfo() + mockThread.mockThreadNumber = .mockRandom() + mockThread.mockCrashed = .random() + mockThread.mockStackFrames = mockStackFrames + + let mock = PLCrashReportMock() + mock.mockThreads = [mockThread] + + // When + let threadInfo = try XCTUnwrap(ThreadInfo(from: mockThread, in: mock)) + + // Then + XCTAssertEqual(threadInfo.threadNumber, mockThread.mockThreadNumber) + XCTAssertEqual(threadInfo.crashed, mockThread.mockCrashed) + XCTAssertEqual(threadInfo.stackFrames.count, mockStackFrames.count) + } + + private let systemImagePaths_device = [ + "/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", + "/usr/lib/system/libdyld.dylib" + ] + private let systemImagePaths_simulator = [ + "/Users/john.appleseed/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" + ] + private let userImagePaths_device = [ + "/private/var/containers/Bundle/Application/0000/Example.app/Example", + "/private/var/containers/Bundle/Application/0000/Example.app/Frameworks/DatadogCrashReporting.framework/DatadogCrashReporting" + ] + private let userImagePaths_simulator = [ + "/Users/john.appleseed/Library/Developer/CoreSimulator/Devices/0000/data/Containers/Bundle/Application/0000/Example.app/Example", + "/Users/john.appleseed/Library/Developer/Xcode/DerivedData/Datadog-abcd/Build/Products/Release-iphonesimulator/DatadogCrashReporting.framework/DatadogCrashReporting" + ] + + func testItDetectsSystemImages() throws { + for systemImagePath in systemImagePaths_device { + XCTAssertTrue(BinaryImageInfo.isPathSystemImageInDevice(systemImagePath), "\(systemImagePath) is a system image") + } + for systemImagePath in systemImagePaths_simulator { + XCTAssertTrue(BinaryImageInfo.isPathSystemImageInSimulator(systemImagePath), "\(systemImagePath) is a system image") + } + for userImagePath in userImagePaths_device { + XCTAssertFalse(BinaryImageInfo.isPathSystemImageInDevice(userImagePath), "\(userImagePath) is an user image") + } + for userImagePath in userImagePaths_simulator { + XCTAssertFalse(BinaryImageInfo.isPathSystemImageInSimulator(userImagePath), "\(userImagePath) is an user image") + } + } + + func testItReadsBinaryImageInfo() throws { + func mock(with imagePath: URL) -> PLCrashReportMock.BinaryImageInfo { + let mock = PLCrashReportMock.BinaryImageInfo() + mock.mockImageUUID = .mockRandom() + mock.mockHasImageUUID = true + mock.mockImageName = imagePath.path + mock.mockImageBaseAddress = .mockRandom() + mock.mockImageSize = .mockRandom() + mock.mockCodeType = .init() + mock.mockCodeType.mockTypeEncoding = PLCrashReportProcessorTypeEncodingMach + mock.mockCodeType.mockType = UInt64(CPU_TYPE_X86_64) + return mock + } + + // Given + #if targetEnvironment(simulator) + let systemImagePath = URL(string: systemImagePaths_simulator.randomElement()!)! + let userImagePath = URL(string: userImagePaths_simulator.randomElement()!)! + #else + let systemImagePath = URL(string: systemImagePaths_device.randomElement()!)! + let userImagePath = URL(string: userImagePaths_device.randomElement()!)! + #endif + + let mockSystemImage = mock(with: systemImagePath) + let mockUserImage = mock(with: userImagePath) + + // When + let systemBinaryImageInfo = try XCTUnwrap(BinaryImageInfo(from: mockSystemImage)) + let userBinaryImageInfo = try XCTUnwrap(BinaryImageInfo(from: mockUserImage)) + + // Then + XCTAssertEqual(systemBinaryImageInfo.uuid, mockSystemImage.mockImageUUID) + XCTAssertEqual(systemBinaryImageInfo.imageName, systemImagePath.lastPathComponent) + XCTAssertTrue(systemBinaryImageInfo.isSystemImage, "\(systemImagePath) is a system image") + XCTAssertEqual(systemBinaryImageInfo.imageBaseAddress, mockSystemImage.mockImageBaseAddress) + XCTAssertEqual(systemBinaryImageInfo.imageSize, mockSystemImage.mockImageSize) + XCTAssertEqual(systemBinaryImageInfo.codeType?.architectureName, "x86_64") + + XCTAssertEqual(userBinaryImageInfo.uuid, mockUserImage.mockImageUUID) + XCTAssertEqual(userBinaryImageInfo.imageName, userImagePath.lastPathComponent) + XCTAssertFalse(userBinaryImageInfo.isSystemImage, "\(userImagePath) is a user image") + XCTAssertEqual(userBinaryImageInfo.imageBaseAddress, mockUserImage.mockImageBaseAddress) + XCTAssertEqual(userBinaryImageInfo.imageSize, mockUserImage.mockImageSize) + XCTAssertEqual(userBinaryImageInfo.codeType?.architectureName, "x86_64") + } + + func testItReadsCodeType() { + typealias ProcessorInfo = PLCrashReportMock.BinaryImageInfo.ProcessorInfo + typealias CodeType = BinaryImageInfo.CodeType + + func mockKnownProcessorInfo(type: UInt64, subtype: UInt64) -> ProcessorInfo { + let mock = ProcessorInfo() + mock.mockTypeEncoding = PLCrashReportProcessorTypeEncodingMach + mock.mockType = type + mock.mockSubtype = subtype + return mock + } + + func mockUnknownProcessorInfo() -> ProcessorInfo { + let mock = ProcessorInfo() + mock.mockTypeEncoding = PLCrashReportProcessorTypeEncodingUnknown + mock.mockType = .mockRandom() + mock.mockSubtype = .mockRandom() + return mock + } + + var mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_X86), subtype: .mockRandom()) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "i386") + + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_X86_64), subtype: .mockRandom()) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "x86_64") + + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM), subtype: .mockRandom()) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "arm") + + // We use XOR to get a value different than any XOR component: + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM ^ CPU_TYPE_X86_64 ^ CPU_TYPE_ARM ^ CPU_TYPE_ARM64), subtype: .mockRandom()) + XCTAssertNil(CodeType(from: mock)?.architectureName) + + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM64), subtype: UInt64(CPU_SUBTYPE_ARM64_ALL)) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "arm64") + + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM64), subtype: UInt64(CPU_SUBTYPE_ARM64_V8)) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "armv8") + + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM64), subtype: UInt64(CPU_SUBTYPE_ARM64E)) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "arm64e") + + // We use XOR to get a value different than any XOR component: + mock = mockKnownProcessorInfo(type: UInt64(CPU_TYPE_ARM64), subtype: UInt64(CPU_SUBTYPE_ARM64_ALL ^ CPU_SUBTYPE_ARM64_V8 ^ CPU_SUBTYPE_ARM64E)) + XCTAssertEqual(CodeType(from: mock)?.architectureName, "arm64-unknown") + + mock = mockUnknownProcessorInfo() + XCTAssertNil(CodeType(from: mock)) + } + + func testItReadsStackFrameWhenItsBinaryImageIsFound() { + // Given + let stackFrameNumber: Int = .mockRandom() + let imagePath: URL = .mockRandomPath() + let mockImage = PLCrashReportMock.BinaryImageInfo() + mockImage.mockHasImageUUID = true + mockImage.mockImageUUID = .mockRandom() + mockImage.mockImageName = imagePath.path + mockImage.mockImageBaseAddress = .mockRandom() + + let mockStackFrame = PLCrashReportMock.StackFrame() + mockStackFrame.mockInstructionPointer = .mockRandom() + + // When + let mock = PLCrashReportMock() + mock.mockImageForAddress[mockStackFrame.mockInstructionPointer] = mockImage // register mock image + + let stackFrameInfo = StackFrame(from: mockStackFrame, number: stackFrameNumber, in: mock) + + // Then + XCTAssertEqual(stackFrameInfo.number, stackFrameNumber) + XCTAssertEqual(stackFrameInfo.instructionPointer, mockStackFrame.mockInstructionPointer) + XCTAssertEqual(stackFrameInfo.libraryName, imagePath.lastPathComponent) + XCTAssertEqual(stackFrameInfo.libraryBaseAddress, mockImage.mockImageBaseAddress) + } + + func testItReadsStackFrameWhenItsBinaryImageIsNotFound() { + // Given + let stackFrameNumber: Int = .mockRandom() + let mockStackFrame = PLCrashReportMock.StackFrame() + mockStackFrame.mockInstructionPointer = .mockRandom() + + // When + let mock = PLCrashReportMock() + mock.mockImageForAddress = [:] // do not register any image + + let stackFrameInfo = StackFrame(from: mockStackFrame, number: stackFrameNumber, in: mock) + + // Then + XCTAssertEqual(stackFrameInfo.number, stackFrameNumber) + XCTAssertEqual(stackFrameInfo.instructionPointer, mockStackFrame.mockInstructionPointer) + XCTAssertNil(stackFrameInfo.libraryName) + XCTAssertNil(stackFrameInfo.libraryBaseAddress) + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportBuilderTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportBuilderTests.swift new file mode 100644 index 0000000000..367a9c13bf --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportBuilderTests.swift @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import CrashReporter + +@testable import DatadogCrashReporting + +class DDCrashReportBuilderTests: XCTestCase { + func testItBuildsDDCrashReportFromPLCrashReport() throws { + // Given + let plCrashReport = try generateLiveReport() // live report of the current process + + // When + let builder = DDCrashReportBuilder() + let ddCrashReport = try builder.createDDCrashReport(from: plCrashReport) + + // Then + XCTAssertGreaterThan(ddCrashReport.threads.count, 0, "Some thread(s) should be recorded") + XCTAssertGreaterThan(ddCrashReport.binaryImages.count, 0, "Some binary image(s) should be recorded") + + // Because `plCrashReport` is generated for current process (it changes dynamically between + // test runs) we cannot assert exact values in exported `DDCrashReport`. Instead, we assert + // some of its properties: + XCTAssertEqual( + plCrashReport.threads?.count, + ddCrashReport.threads.count, + "`DDCrashReport` should include the same number of threads as `PLCrashReport`" + ) + XCTAssertTrue( + ddCrashReport.stack.contains("DatadogCrashReportingTests"), + "`DDCrashReport's` stack should include at least one frame from `DatadogCrashReportingTests` image" + ) + XCTAssertTrue( + ddCrashReport.stack.contains("XCTest"), + "`DDCrashReport's` stack should include at least one frame from `XCTest` image" + ) + #if os(iOS) + XCTAssertTrue( + ddCrashReport.binaryImages.contains(where: { $0.libraryName == "DatadogCrashReportingTests iOS" }), + "`DDCrashReport` should include the image for `DatadogCrashReportingTests iOS`" + ) + #elseif os(tvOS) + XCTAssertTrue( + ddCrashReport.binaryImages.contains(where: { $0.libraryName == "DatadogCrashReportingTests tvOS" }), + "`DDCrashReport` should include the image for `DatadogCrashReportingTests tvOS`" + ) + #endif + XCTAssertTrue( + // Assert on prefix as it's `XCTestCore` on iOS 15+ and `XCTest` earlier: + ddCrashReport.binaryImages.contains(where: { $0.libraryName.hasPrefix("XCTest") }), + "`DDCrashReport` should include the image for `XCTest`" + ) + } + + // MARK: - Helper + + /// This method generates "live report" using `PLCrashReporter`. + /// When calling `generateLiveReportAndReturnError()`, PLCR generates `PLCrashReport` object describing + /// the running process. It doesn't issue any crash - just records running threads and binary images. + private func generateLiveReport() throws -> PLCrashReport { + let configuration = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: []) + let crashReporter = try XCTUnwrap(PLCrashReporter(configuration: configuration)) + try crashReporter.enableAndReturnError() + + let liveReportData = try crashReporter.generateLiveReportAndReturnError() + let liveReport = try PLCrashReport(data: liveReportData) + return liveReport + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift new file mode 100644 index 0000000000..7ffcfa6c32 --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/DDCrashReportExporterTests.swift @@ -0,0 +1,494 @@ +/* + * 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 CrashReporter + +@testable import DatadogCrashReporting + +class DDCrashReportExporterTests: XCTestCase { + private let exporter = DDCrashReportExporter() + private var crashReport: CrashReport = .mockAny() + + // MARK: - Formatting `error.type` + + func testExportingErrorType() { + crashReport.signalInfo = .init(name: "SIGNAME", code: "SIG_CODE", address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).type, "SIGNAME (SIG_CODE)") + + crashReport.signalInfo = .init(name: "SIGNAME", code: nil, address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).type, "SIGNAME ()") + + crashReport.signalInfo = .init(name: nil, code: "SIG_CODE", address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).type, " (SIG_CODE)") + + crashReport.signalInfo = .init(name: nil, code: nil, address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).type, " ()") + } + + // MARK: - Formatting `error.message` + + func testExportingErrorMessageFromExceptionInfo() { + crashReport.exceptionInfo = .init(name: "ExceptionName", reason: "Exception reason", stackFrames: []) + XCTAssertEqual( + exporter.export(crashReport).message, + "Terminating app due to uncaught exception 'ExceptionName', reason: 'Exception reason'." + ) + + crashReport.exceptionInfo = .init(name: "ExceptionName", reason: nil, stackFrames: []) + XCTAssertEqual( + exporter.export(crashReport).message, + "Terminating app due to uncaught exception 'ExceptionName', reason: ''." + ) + + crashReport.exceptionInfo = .init(name: nil, reason: "Exception reason", stackFrames: []) + XCTAssertEqual( + exporter.export(crashReport).message, + "Terminating app due to uncaught exception '', reason: 'Exception reason'." + ) + } + + func testExportingErrorMessageFromSignalInfo() throws { + let signalNames = [ + "SIGSIGNAL 0", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", + "SIGABRT", "SIGEMT", "SIGFPE", "SIGKILL", "SIGBUS", "SIGSEGV", "SIGSYS", + "SIGPIPE", "SIGALRM", "SIGTERM", "SIGURG", "SIGSTOP", "SIGTSTP", "SIGCONT", + "SIGCHLD", "SIGTTIN", "SIGTTOU", "SIGIO", "SIGXCPU", "SIGXFSZ", "SIGVTALRM", + "SIGPROF", "SIGWINCH", "SIGINFO", "SIGUSR1", "SIGUSR2" + ] + + func readSignalDescriptionFromOS(signalName: String) -> String? { + let knownSignalNames = Mirror(reflecting: sys_signame) + .children + .compactMap { $0.value as? UnsafePointer } + .map { String(cString: $0).uppercased() } // [HUP, INT, QUIT, ILL, TRAP, ABRT, ...] + + let knownSignalDescriptions = Mirror(reflecting: sys_siglist) + .children + .compactMap { $0.value as? UnsafePointer } + .map { String(cString: $0) } // [Hangup, Interrupt, Quit, Illegal instruction, ...] + + XCTAssertEqual(knownSignalNames.count, knownSignalDescriptions.count) // sanity check + + if let index = knownSignalNames.firstIndex(where: { signalName == "SIG\($0)" }) { + return knownSignalDescriptions[index] + } else { + return nil + } + } + + signalNames.forEach { signalName in + crashReport.signalInfo = .init(name: signalName, code: .mockAny(), address: .mockAny()) + + let expectedSignalDescription = readSignalDescriptionFromOS(signalName: signalName) + let expectedMessage = "Application crash: \(signalName) (\(expectedSignalDescription!))" + + XCTAssertEqual(exporter.export(crashReport).message, expectedMessage) + } + } + + func testExportingErrorMessageWhenBothSignalAndExceptionInfoAreUnavailable() { + crashReport.signalInfo = nil + crashReport.exceptionInfo = nil + XCTAssertEqual(exporter.export(crashReport).message, "Application crash: ") + } + + func testExportingErrorMessageWhenBothSignalAndExceptionInfoAreAvailable() { + crashReport.signalInfo = .init(name: "SIGHUP", code: .mockAny(), address: .mockAny()) + crashReport.exceptionInfo = .init(name: "ExceptionName", reason: "Exception reason", stackFrames: []) + XCTAssertEqual( + exporter.export(crashReport).message, + "Terminating app due to uncaught exception 'ExceptionName', reason: 'Exception reason'.", + "It should prefer exception information" + ) + } + + // MARK: - Formatting `error.stack` + + func testExportingErrorStackFromExceptionInfo() { + let stackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + .init(number: 2, libraryName: "Bar", libraryBaseAddress: 300, instructionPointer: 302), + .init(number: 3, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + ] + + crashReport.exceptionInfo = .init(name: .mockAny(), reason: .mockAny(), stackFrames: stackFrames) + let expectedStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 2 Bar 0x000000000000012e 0x12c + 2 + 3 Bizz 0x00000000000001b0 0x190 + 32 + """ + + XCTAssertEqual(exporter.export(crashReport).stack, expectedStack) + } + + func testExportingErrorStackFromThreadInfo() { + let crashedThreadStackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + .init(number: 2, libraryName: "Bar", libraryBaseAddress: 300, instructionPointer: 302), + .init(number: 3, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + ] + let otherThreadStackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 110), + .init(number: 1, libraryName: "Bazz", libraryBaseAddress: 500, instructionPointer: 550), + ] + + crashReport.exceptionInfo = nil + crashReport.threads = [ + .init(threadNumber: 0, crashed: false, stackFrames: otherThreadStackFrames), + .init(threadNumber: 1, crashed: true, stackFrames: crashedThreadStackFrames), + .init(threadNumber: 2, crashed: false, stackFrames: otherThreadStackFrames), + ] + + let expectedStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 2 Bar 0x000000000000012e 0x12c + 2 + 3 Bizz 0x00000000000001b0 0x190 + 32 + """ + + XCTAssertEqual(exporter.export(crashReport).stack, expectedStack) + } + + func testExportingErrorStackWhenBothThreadAndExceptionInfoAreUnavailable() { + crashReport.exceptionInfo = nil + crashReport.threads = [] + XCTAssertEqual(exporter.export(crashReport).stack, "???") + } + + func testExportingVeryLongErrorStack() { + let stackFrames: [StackFrame] = (0..<10_024).map { index in + StackFrame( + number: index, + libraryName: "VeryLongLibraryName-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-\(index)", + libraryBaseAddress: 100, + instructionPointer: 102 + ) + } + crashReport.exceptionInfo = .init(name: .mockAny(), reason: .mockAny(), stackFrames: stackFrames) + + let exportedStack = exporter.export(crashReport).stack + let lastExportedStackFrame = exportedStack.split(separator: "\n").last! + + XCTAssertEqual( + "10023 VeryLongLibraryName-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-10023 0x0000000000000066 0x64 + 2", + lastExportedStackFrame + ) + } + + func testWhenSomeSucceedingFramesInTheStackAreMissing_itPrintsNonconsecutiveFrameNumbers() { + let stackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + // missing line number: 2 + .init(number: 3, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + // missing line number: 4 + .init(number: 5, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + ] + + crashReport.exceptionInfo = .init(name: .mockAny(), reason: .mockAny(), stackFrames: stackFrames) + let actualStack = exporter.export(crashReport).stack + let expectedStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 3 Bizz 0x00000000000001b0 0x190 + 32 + 5 Bizz 0x00000000000001b0 0x190 + 32 + """ + + XCTAssertEqual(actualStack, expectedStack) + } + + func testWhenLastFrameInTheStackHasNoLibraryBaseAddress_itIsFilteredOut() { + let stackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + .init(number: 2, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + .init(number: 3, libraryName: "Bizz", libraryBaseAddress: nil, instructionPointer: 432), + ] + + crashReport.exceptionInfo = .init(name: .mockAny(), reason: .mockAny(), stackFrames: stackFrames) + + let actualStack = exporter.export(crashReport).stack + let expectedStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 2 Bizz 0x00000000000001b0 0x190 + 32 + """ + + XCTAssertEqual(actualStack, expectedStack) + } + + // MARK: - Formatting threads + + func testExportingThreads() { + let crashedThreadStackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + .init(number: 2, libraryName: "Bar", libraryBaseAddress: 300, instructionPointer: 302), + .init(number: 3, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + ] + let otherThreadStackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 110), + .init(number: 1, libraryName: "Bazz", libraryBaseAddress: 500, instructionPointer: 550), + ] + + crashReport.threads = [ + .init(threadNumber: 0, crashed: false, stackFrames: otherThreadStackFrames), + .init(threadNumber: 1, crashed: true, stackFrames: crashedThreadStackFrames), + .init(threadNumber: 2, crashed: false, stackFrames: otherThreadStackFrames), + ] + + let exportedThreads = exporter.export(crashReport).threads + + let expectedCrashedThreadStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 2 Bar 0x000000000000012e 0x12c + 2 + 3 Bizz 0x00000000000001b0 0x190 + 32 + """ + let expectedOtherThreadStack = """ + 0 Foo 0x000000000000006e 0x64 + 10 + 1 Bazz 0x0000000000000226 0x1f4 + 50 + """ + + XCTAssertEqual(exportedThreads.count, 3) + XCTAssertEqual(exportedThreads[0].name, "Thread 0") + XCTAssertFalse(exportedThreads[0].crashed) + XCTAssertEqual(exportedThreads[0].stack, expectedOtherThreadStack) + + XCTAssertEqual(exportedThreads[1].name, "Thread 1") + XCTAssertTrue(exportedThreads[1].crashed) + XCTAssertEqual(exportedThreads[1].stack, expectedCrashedThreadStack) + + XCTAssertEqual(exportedThreads[2].name, "Thread 2") + XCTAssertFalse(exportedThreads[2].crashed) + XCTAssertEqual(exportedThreads[2].stack, expectedOtherThreadStack) + } + + func testWhenLastFrameInThreadStackHasNoLibraryBaseAddress_itIsNotFilteredOut() { + let crashedThreadStackFrames: [StackFrame] = [ + .init(number: 0, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 102), + .init(number: 1, libraryName: "Foo", libraryBaseAddress: 100, instructionPointer: 112), + .init(number: 2, libraryName: "Bizz", libraryBaseAddress: 400, instructionPointer: 432), + .init(number: 3, libraryName: nil, libraryBaseAddress: nil, instructionPointer: 432), + ] + + crashReport.threads = [ + .init(threadNumber: 0, crashed: true, stackFrames: crashedThreadStackFrames), + ] + + let actualStack = exporter.export(crashReport).threads[0].stack + let expectedStack = """ + 0 Foo 0x0000000000000066 0x64 + 2 + 1 Foo 0x0000000000000070 0x64 + 12 + 2 Bizz 0x00000000000001b0 0x190 + 32 + 3 ??? 0x00000000000001b0 0x0 + 0 + """ + + XCTAssertEqual(actualStack, expectedStack) + } + + // MARK: - Formatting binary images + + func testExportingBinaryImages() { + let architectureName: String = .mockRandom() + crashReport.binaryImages = (0..<10).map { index in + .mockWith( + uuid: "uuid-\(index)", + imageName: "image\(index)", + isSystemImage: index % 2 == 0, + architectureName: architectureName + ) + } + + let exportedImages = exporter.export(crashReport).binaryImages + + XCTAssertEqual(exportedImages.count, 10) + exportedImages.enumerated().forEach { index, exportedImage in + XCTAssertEqual(exportedImage.uuid, "uuid-\(index)") + XCTAssertEqual(exportedImage.libraryName, "image\(index)") + XCTAssertEqual(exportedImage.isSystemLibrary, index % 2 == 0) + XCTAssertEqual(exportedImage.architecture, architectureName) + } + } + + func testExportingBinaryImageWhenUUIDIsUnavailable() { + // Given + crashReport.binaryImages = [.mockWith(uuid: nil)] + + // When + let exportedImages = exporter.export(crashReport).binaryImages + + // Then + XCTAssertEqual(exportedImages.first?.uuid, "???") + } + + func testExportingBinaryImageAddressRange() throws { + let randomImageLoadAddress: UInt64 = .mockRandom() + let randomImageSize: UInt64 = .mockRandom() + + crashReport.binaryImages = [.mockWith(imageBaseAddress: randomImageLoadAddress, imageSize: randomImageSize)] + let exportedImage = try XCTUnwrap(exporter.export(crashReport).binaryImages.first) + + let expectedLoadAddress = "0x" + randomImageLoadAddress.toHex + let offset = max(1, randomImageSize.subtractIfNoOverflow(1) ?? randomImageSize) + let expectedMaxAddress = "0x" + (randomImageLoadAddress.addIfNoOverflow(offset) ?? randomImageLoadAddress).toHex + + XCTAssertEqual(exportedImage.loadAddress, expectedLoadAddress) + XCTAssertEqual(exportedImage.maxAddress, expectedMaxAddress) + } + + // MARK: - Formatting other values + + func testExportingReportDate() { + let randomDate: Date = .mockRandomInThePast() + crashReport.systemInfo = .init(timestamp: randomDate) + XCTAssertEqual(exporter.export(crashReport).date, randomDate) + } + + func testExportingIncidentIdentifier() { + let randomIdentifier: String = .mockRandom() + crashReport.incidentIdentifier = randomIdentifier + XCTAssertEqual(exporter.export(crashReport).meta.incidentIdentifier, randomIdentifier) + } + + func testExportingProcess() { + let randomName: String = .mockRandom() + let randomID: UInt = .mockRandom() + crashReport.processInfo = .mockWith( + processName: randomName, + processID: randomID + ) + XCTAssertEqual(exporter.export(crashReport).meta.process, "\(randomName) [\(randomID)]") + } + + func testExportingProcessID() { + let randomID: UInt = .mockRandom() + crashReport.processInfo = .mockWith( + processName: nil, + processID: randomID + ) + XCTAssertEqual(exporter.export(crashReport).meta.process, "[\(randomID)]") + } + + func testExportingParentProcess() { + let randomName: String = .mockRandom() + let randomID: UInt = .mockRandom() + + crashReport.processInfo = .mockWith(parentProcessID: randomID, parentProcessName: randomName) + XCTAssertEqual(exporter.export(crashReport).meta.parentProcess, "\(randomName) [\(randomID)]") + + crashReport.processInfo = .mockWith(parentProcessID: randomID, parentProcessName: nil) + XCTAssertEqual(exporter.export(crashReport).meta.parentProcess, "[\(randomID)]") + } + + func testExportingProcessPath() { + let randomPath: String = .mockRandom() + crashReport.processInfo = .mockWith(processPath: randomPath) + XCTAssertEqual(exporter.export(crashReport).meta.path, randomPath) + } + + func testExportingCodeType() { + let randomArchitectureName: String = .mockRandom() + crashReport.binaryImages = [.mockWith(architectureName: randomArchitectureName)] + XCTAssertEqual(exporter.export(crashReport).meta.codeType, randomArchitectureName) + } + + func testExportingExceptionType() { + let randomName: String = .mockRandom() + crashReport.signalInfo = .init(name: randomName, code: .mockAny(), address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).meta.exceptionType, randomName) + } + + func testExportingExceptionCodes() { + let randomCode: String = .mockRandom() + crashReport.signalInfo = .init(name: .mockAny(), code: randomCode, address: .mockAny()) + XCTAssertEqual(exporter.export(crashReport).meta.exceptionCodes, randomCode) + } + + func testExportingContext() throws { + let randomData: Data = .mockRandom() + crashReport.contextData = randomData + XCTAssertEqual(exporter.export(crashReport).context, randomData) + } + + func testExportingAdditionalTelemetry() { + let randomFlag: Bool = .random() + crashReport.wasTruncated = randomFlag + XCTAssertEqual(exporter.export(crashReport).wasTruncated, randomFlag) + } + + // MARK: - Comparing with PLCR text format + + func testExportedStacksHaveTheSameFormatAndValuesAsIfTheyWereExportedFromPLCR() throws { + let crashReporter = try PLCrashReporter(configuration: .ddConfiguration())! + + // Given + let plCrashReport = try PLCrashReport( + data: try crashReporter.generateLiveReportAndReturnError() + ) + + // When + let exporter = DDCrashReportExporter() + let ddCrashReport = exporter.export(try CrashReport(from: plCrashReport)) + + // Then + let plcrTextFormat = PLCrashReportTextFormatter.stringValue(for: plCrashReport, with: PLCrashReportTextFormatiOS)! + + ddCrashReport.threads.forEach { thread in + XCTAssertTrue( + plcrTextFormat.contains(thread.stack), + """ + Stack: + ``` + \(thread.stack) + ``` + + does not appear in PLCR text format: + ``` + \(plcrTextFormat) + ``` + """ + ) + } + } + + func testExportedBinaryImagesHaveTheSameValuesAsIfTheyWereExportedFromPLCR() throws { + let crashReporter = try PLCrashReporter(configuration: .ddConfiguration())! + + // Given + let plCrashReport = try PLCrashReport( + data: try crashReporter.generateLiveReportAndReturnError() + ) + + // When + let exporter = DDCrashReportExporter() + let ddCrashReport = exporter.export(try CrashReport(from: plCrashReport)) + + // Then + let plcrTextFormat = PLCrashReportTextFormatter.stringValue(for: plCrashReport, with: PLCrashReportTextFormatiOS)! + let plcrByLines = plcrTextFormat.split(separator: "\n").reversed() // matching in reversed report is 2x faster + + ddCrashReport.binaryImages.forEach { binaryImage in + XCTAssertTrue( + plcrByLines.contains { line in + // PLCR uses free-form text format, e.g.: + // ` 0x10ce2e000 - 0x10ced9fff +Example x86_64 /.../Example.app/Example` + // Instead of matching the whole line, just checking if all values appear in the line should be enough: + let matchLoadAddress = line.contains(binaryImage.loadAddress) + let matchMaxAddress = line.contains(binaryImage.maxAddress) + let matchArchitecture = line.contains(binaryImage.architecture) + let matchLibraryName = line.contains(binaryImage.libraryName) + let matchUUID = line.contains(binaryImage.uuid) + return matchLoadAddress && matchMaxAddress && matchArchitecture && matchLibraryName && matchUUID + } + ) + } + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/PLCrashReporterIntegrationTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/PLCrashReporterIntegrationTests.swift new file mode 100644 index 0000000000..bee06a275c --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/PLCrashReporterIntegrationTests.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 XCTest +@testable import DatadogCrashReporting +import CrashReporter + +class PLCrashReporterIntegrationTests: XCTestCase { + func testGivenPLCrashReporter_whenInitializingWithDDConfig_itSetsCustomPath() throws { + // Given + let configuration = try PLCrashReporterConfig.ddConfiguration() + + // When + let reporter = PLCrashReporter(configuration: configuration) + + // Then + let reporterPath = reporter?.crashReportPath() + let expected = "/Library/Caches/com.datadoghq.crash-reporting/v1/" + XCTAssertTrue(reporterPath?.contains(expected) ?? false) + } +} diff --git a/DatadogCrashReporting/Tests/PLCrashReporterIntegration/Utils/SwiftExtensionTests.swift b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/Utils/SwiftExtensionTests.swift new file mode 100644 index 0000000000..5cc70ee88b --- /dev/null +++ b/DatadogCrashReporting/Tests/PLCrashReporterIntegration/Utils/SwiftExtensionTests.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 +import TestUtilities +@testable import DatadogCrashReporting + +class UInt64Tests: XCTestCase { + func testSubtractIfNoOverflow() { + let random1: UInt64 = .mockRandom() + let random2: UInt64 = .mockRandom(otherThan: random1) + + let big = max(random1, random2) + let small = min(random1, random2) + + XCTAssertEqual(big.subtractIfNoOverflow(small), big - small) + XCTAssertNil(small.subtractIfNoOverflow(big), "It should cause the overflow and return `nil`") + } + + func testAddIfNoOverflow() { + let random: UInt64 = .mockRandom(otherThan: 0) + + let distanceToMax = UInt64.max - random + let overflowAddition = distanceToMax + 1 + let safeAddition = distanceToMax - 1 + + XCTAssertEqual(random.addIfNoOverflow(safeAddition), random + safeAddition) + XCTAssertNil(random.addIfNoOverflow(overflowAddition), "It should cause the overflow and return `nil`") + } +} + +class StringTests: XCTestCase { + func testAddPrefix() { + let originalString: String = .mockRandom() + + let prefixLength: Int = .mockRandom(min: 1, max: 100) + let targetLength = prefixLength + originalString.count + + let prefixCharacter: Character = "x" + let expectedPrefix = String(repeating: prefixCharacter, count: prefixLength) + + let modifiedString = originalString.addPrefix(repeating: prefixCharacter, targetLength: targetLength) + + XCTAssertFalse(originalString.hasPrefix(expectedPrefix)) + XCTAssertTrue(modifiedString.hasPrefix(expectedPrefix)) + } + + func testAddSuffix() { + let originalString: String = .mockRandom() + + let suffixLength: Int = .mockRandom(min: 1, max: 100) + let targetLength = suffixLength + originalString.count + + let suffixCharacter: Character = "x" + let expectedSuffix = String(repeating: suffixCharacter, count: suffixLength) + + let modifiedString = originalString.addSuffix(repeating: suffixCharacter, targetLength: targetLength) + + XCTAssertFalse(originalString.hasSuffix(expectedSuffix)) + XCTAssertTrue(modifiedString.hasSuffix(expectedSuffix)) + } +} diff --git a/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift b/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift new file mode 100644 index 0000000000..7cd5b7f3ca --- /dev/null +++ b/DatadogExtensions/Alamofire/DatadogAlamofireExtension.swift @@ -0,0 +1,61 @@ +/* + * 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 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? + + private var interceptor: URLSessionInterceptor? { + let core = self.core ?? CoreRegistry.default + return URLSessionInterceptor.shared(in: core) + } + + public required init(core: DatadogCoreProtocol? = nil ) { + self.core = core + } + + public func request(_ request: Request, didCreateTask task: URLSessionTask) { + interceptor?.intercept(task: task) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + interceptor?.task(task, didFinishCollecting: metrics) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + interceptor?.task(task, didCompleteWithError: error) + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + interceptor?.task(dataTask, didReceive: data) + } +} + +/// 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? + + private var interceptor: URLSessionInterceptor? { + let core = self.core ?? CoreRegistry.default + return URLSessionInterceptor.shared(in: core) + } + + public required init(core: DatadogCoreProtocol? = nil ) { + self.core = core + } + + public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + let instrumentedRequest = interceptor?.intercept(request: urlRequest) ?? urlRequest + completion(.success(instrumentedRequest)) + } +} diff --git a/DatadogExtensions/Alamofire/README.md b/DatadogExtensions/Alamofire/README.md new file mode 100644 index 0000000000..e843d3bc11 --- /dev/null +++ b/DatadogExtensions/Alamofire/README.md @@ -0,0 +1,58 @@ +## **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. +It's a counterpart of `DDURLSessionDelegate`, which is provided for native `URLSession` instrumentation. + +## Getting started + +### CocoaPods + +To include the Datadog integration for [Alamofire][1] in your project, add the +following to your `Podfile`: +```ruby +pod 'DatadogSDKAlamofireExtension' +``` +`DatadogSDKAlamofireExtension` requires Datadog SDK `1.5.0` or higher and `Alamofire 5.0` or higher. + +### Carthage and SPM + +The Datadog [Alamofire][1] integration doesn't support [Carthage][2] or [SPM][3], however, the code needed for set up is very low. You may want to include the source files from this folder directly in your project. + +### Initial setup + +Follow the regular steps for initializing Datadog SDK for [Tracing][4] or [RUM][5]. + +Instead of using `DDURLSessionDelegate` for `URLSession`, use `DDEventMonitor` and `DDRequestInterceptor` for `Alamofire.Session`: + +```swift +import DatadogAlamofireExtension +import Alamofire + +let alamofireSession = Session( + interceptor: DDRequestInterceptor(), + eventMonitors: [DDEventMonitor()] +) +``` + +Using this setup makes the Datadog SDK track requests from this instance of the `Alamofire.Session`. + +## Contributing + +Pull requests are welcome. First, open an issue to discuss what you would like to change. For more information, read the [Contributing Guide](../../../CONTRIBUTING.md). + +## License + +[Apache License, v2.0](../../../LICENSE) + +[1]: https://github.com/Alamofire/Alamofire +[2]: https://github.com/Carthage/Carthage +[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 new file mode 100644 index 0000000000..96a22a4b81 --- /dev/null +++ b/DatadogInternal.podspec @@ -0,0 +1,26 @@ +Pod::Spec.new do |s| + s.name = "DatadogInternal" + s.version = "2.22.0" + s.summary = "Datadog Internal Package. This module is not for public use." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + 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 } + + s.source_files = ["DatadogInternal/Sources/**/*.swift"] + +end diff --git a/DatadogInternal/Sources/Attributes/Attributes.swift b/DatadogInternal/Sources/Attributes/Attributes.swift new file mode 100644 index 0000000000..362374d48b --- /dev/null +++ b/DatadogInternal/Sources/Attributes/Attributes.swift @@ -0,0 +1,201 @@ +/* + * 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 `String` value naming the attribute. +/// +/// Dot syntax can be used to nest objects: +/// +/// logger.addAttribute(forKey: "person.name", value: "Adam") +/// logger.addAttribute(forKey: "person.age", value: 32) +/// +/// // When seen in Datadog console: +/// { +/// person: { +/// name: "Adam" +/// age: 32 +/// } +/// } +/// +/// - Important +/// Values can be nested up to 8 levels deep. Keys using more than 8 levels will be sanitized by the SDK. +/// +public typealias AttributeKey = String + +/// Any `Encodable` value of the attribute (`String`, `Int`, `Bool`, `Date` etc.). +/// +/// Custom `Encodable` types are supported as well with nested encoding containers: +/// +/// struct Person: Codable { +/// let name: String +/// let age: Int +/// let address: Address +/// } +/// +/// struct Address: Codable { +/// let city: String +/// let street: String +/// } +/// +/// let address = Address(city: "Paris", street: "Champs Elysees") +/// let person = Person(name: "Adam", age: 32, address: address) +/// +/// // When seen in Datadog console: +/// { +/// person: { +/// name: "Adam" +/// age: 32 +/// address: { +/// city: "Paris", +/// street: "Champs Elysees" +/// } +/// } +/// } +/// +/// - Important +/// Attributes in Datadog console can be nested up to 10 levels deep. If number of nested attribute levels +/// defined as sum of key levels and value levels exceeds 10, the data may not be delivered. +/// +public typealias AttributeValue = Encodable + +// MARK: - Internal attributes + +/// Internal attributes, passed from cross-platform bridge. +/// Used to configure or override SDK internal features and attributes for the need of cross-platform SDKs (e.g. React Native SDK). +public struct CrossPlatformAttributes { + /// Custom app version passed from CP SDK. Used for all events issued by the SDK (both coming from cross-platform SDK and produced internally, like RUM long tasks). + /// It should replace the default native `version` read from `Info.plist`. + /// Expects `String` value (semantic version). + public static let version: String = "_dd.version" + + /// Custom SDK version passed from CP SDK. Used for all events issued by the SDK (both coming from cross-platform SDK and produced internally, like RUM long tasks). + /// It should replace the default native `sdkVersion`. + /// Expects `String` value (semantic version). + public static let sdkVersion: String = "_dd.sdk_version" + + /// Custom SDK `source` passed from CP SDK. Used for all events issued by the SDK (both coming from cross-platform SDK and produced internally, like RUM long tasks). + /// It should replace the default native `ddsource` value (`"ios"`). + /// Expects `String` value. + public static let ddsource: String = "_dd.source" + + /// Custom Variant passed from a CP SDK. This is the 'flavor' parameter used in Android and Flutter, Used for all events issued by the SDK (both coming from cross-platform + /// SDK and produced internally, like RUM long tasks). + /// It does not replace any default native properties as iOS does not have the concept of 'flavors' or variants. + public static let variant: String = "_dd.variant" + + /// A custom unique id that identifies this build of the application, used from symbolication and deobfuscation + /// Id does not replace any default native properties and is sent in addition to version and build number + public static let buildId: String = "_dd.build_id" + + /// Event timestamp passed from CP SDK. Used for all RUM events issued by cross platform SDK. + /// It should replace event time obtained from `DateProvider` to ensure that events are not skewed due to time difference in native and cross-platform SDKs. + /// Expects `Int64` value (milliseconds). + public static let timestampInMilliseconds = "_dd.timestamp" + + /// Custom "source type" of the error passed from CP SDK. Used in RUM errors reported by cross platform SDK. + /// It names the language or platform of the RUM error stack trace, so the SCI backend knows how to symbolicate it. + /// Expects `String` value. + public static let errorSourceType = "_dd.error.source_type" + + /// Custom attribute of the error passed from CP SDK. Used in RUM errors reported by cross platform SDK. + /// It flags the error has being fatal for the host application. + /// Expects `Bool` value. + public static let errorIsCrash = "_dd.error.is_crash" + + /// Trace ID passed from CP SDK. Used in RUM resources created by cross platform SDK. + /// When cross-platform SDK injects tracing headers to intercepted resource, we pass tracing information through this attribute + /// and send it within the RUM resource, so the RUM backend can issue corresponding APM span on behalf of the mobile app. + /// Expects `String` value. + public static let traceID = "_dd.trace_id" + + /// Span ID passed from CP SDK. Used in RUM resources created by cross platform SDK. + /// When cross-platform SDK injects tracing headers to intercepted resource, we pass tracing information through this attribute + /// and send it within the RUM resource, so the RUM backend can issue corresponding APM span on behalf of the mobile app. + /// Expects `String` value. + public static let spanID = "_dd.span_id" + + /// Trace sample rate applied to RUM resources created by cross platform SDK. + /// We send cross-platform SDK's sample rate within RUM resource in order to provide accurate visibility into what settings are + /// configured at the SDK level. This gets displayed on APM's traffic ingestion control page. + /// Expects `Double` value between `0.0` and `1.0`. + public static let rulePSR = "_dd.rule_psr" + + /// Custom attribute of the log passed from CP SDK. Used in error logs reported by cross platform SDK. + /// It flags the error has being fatal for the host application, so we can prevent creating a duplicate RUM error. + /// The goal of RUMM-3289 is to create an RFC to get rid of this mechanism. + /// Expects `Bool` value. + public static let errorLogIsCrash = "_dd.error_log.is_crash" + + /// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK. + /// It sets the GraphQL operation name if it was defined by the developer. + /// Expects `String` value. + public static let graphqlOperationName = "_dd.graphql.operation_name" + + /// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK. + /// It sets the GraphQL operation type. + /// Expects `String` value of either `query`, `mutation` or `subscription`. + public static let graphqlOperationType = "_dd.graphql.operation_type" + + /// Custom attribute passed when starting GraphQL RUM resources from a cross platform SDK. + /// It sets the GraphQL payload as a JSON string when it is specified. + /// Expects `String` value. + public static let graphqlPayload = "_dd.graphql.payload" + + /// Custom attribute passed when starting GraphQL RUM resources resources from a cross platform SDK. + /// It sets the GraphQL varibles as a JSON string if they were defined by the developer. + /// Expects `String` value. + public static let graphqlVariables = "_dd.graphql.variables" + + /// Override the `source_type` of errors reported by the native crash handler. This is used on + /// platforms that can supply extra steps or information on a native crash (such as Unity's IL2CPP) + public static let nativeSourceType = "_dd.native_source_type" + + /// Add "binary images" to the reportted error to assist with symbolication. Used by Unity for IL2CPP symbolicaiton + public static let includeBinaryImages = "_dd.error.include_binary_images" +} + +public struct LaunchArguments { + /// Each product should consider this argument to offer simple debugging experience. + /// 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/Attributes/AttributesSanitizer.swift b/DatadogInternal/Sources/Attributes/AttributesSanitizer.swift new file mode 100644 index 0000000000..db1a4da8cf --- /dev/null +++ b/DatadogInternal/Sources/Attributes/AttributesSanitizer.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 Foundation + +/// Common attributes sanitizer for all features. +public struct AttributesSanitizer { + public struct Constraints { + /// Maximum number of nested levels in attribute name. E.g. `person.address.street` has 3 levels. + /// If attribute name exceeds this number, extra levels are escaped by using `_` character (`one.two.(...).nine.ten_eleven_twelve`). + public static let maxNestedLevelsInAttributeName: Int = 10 + /// Maximum number of attributes in log. + /// If this number is exceeded, extra attributes will be ignored. + public static let maxNumberOfAttributes: Int = 128 + } + + let featureName: String + + public init(featureName: String) { + self.featureName = featureName + } + + // MARK: - Attribute keys sanitization + + /// Attribute keys can only have `Constants.maxNestedLevelsInAttributeName` levels. + /// Extra levels are escaped with "_", e.g.: + /// + /// one.two.three.four.five.six.seven.eight.nine.ten.eleven + /// + /// becomes: + /// + /// one.two.three.four.five.six.seven.eight_nine_ten_eleven + /// + public func sanitizeKeys(for attributes: [String: Value], prefixLevels: Int = 0) -> [String: Value] { + let sanitizedAttributes: [(String, Value)] = attributes.map { key, value in + let sanitizedName = sanitize(attributeKey: key, prefixLevels: prefixLevels) + if sanitizedName != key { + DD.logger.warn( + """ + \(featureName) attribute '\(key)' was modified to '\(sanitizedName)' to match Datadog constraints. + """ + ) + return (sanitizedName, value) + } else { + return (key, value) + } + } + return Dictionary(uniqueKeysWithValues: sanitizedAttributes) + } + + private func sanitize(attributeKey: String, prefixLevels: Int = 0) -> String { + var dotsCount = prefixLevels + var sanitized = "" + for char in attributeKey { + if char == "." { + dotsCount += 1 + sanitized.append(dotsCount >= Constraints.maxNestedLevelsInAttributeName ? "_" : char) + } else { + sanitized.append(char) + } + } + return sanitized + } + + // MARK: - Attributes count limitting + + /// Removes attributes exceeding the `count` limit. + public func limitNumberOf(attributes: [String: Value], to count: Int) -> [String: Value] { + if attributes.count > count { + let extraAttributesCount = attributes.count - count + DD.logger.warn( + """ + Number of \(featureName) attributes exceeds the limit of \(Constraints.maxNumberOfAttributes). + \(extraAttributesCount) attribute(s) will be ignored. + """ + ) + return Dictionary(uniqueKeysWithValues: attributes.dropLast(extraAttributesCount)) + } else { + return attributes + } + } +} diff --git a/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift new file mode 100644 index 0000000000..ea5af300a9 --- /dev/null +++ b/DatadogInternal/Sources/BacktraceReporting/BacktraceReporter.swift @@ -0,0 +1,93 @@ +/* + * 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 value identifying the thread for `BacktraceReport` generation. +public typealias ThreadID = thread_t + +public extension Thread { + /// Obtains the `ThreadID` of the caller thread. + /// + /// Should be used in conjunction with `BacktraceReporting.generateBacktrace(threadID:)` to generate backtrace of particular thread. + static var currentThreadID: ThreadID { pthread_mach_thread_np(pthread_self()) } +} + +/// A protocol for types capable of generating backtrace reports. +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`). + /// + /// - Parameter threadID: An ID of the thread that backtrace generation should start on. + /// - Returns: A `BacktraceReport` starting on the given thread and containing information about all other threads + /// running in the process. Returns `nil` if the backtrace report cannot be generated. + func generateBacktrace(threadID: ThreadID) throws -> BacktraceReport? +} + +public extension BacktraceReporting { + /// Generates a backtrace report for current thread. + /// + /// The caller thread will be promoted in the main stack of returned `BacktraceReport` (`report.stack`). + /// + /// - Returns: A `BacktraceReport` starting on the current thread and containing information about all other threads + /// running in the process. Returns `nil` if the backtrace report cannot be generated. + func generateBacktrace() throws -> BacktraceReport? { + let callerThreadID = Thread.currentThreadID + return try generateBacktrace(threadID: callerThreadID) + } +} + +internal struct CoreBacktraceReporter: BacktraceReporting, @unchecked Sendable { + /// A weak core reference. + private weak var core: DatadogCoreProtocol? + + /// Creates backtrace reporter associated with a core instance. + /// + /// The `CoreBacktraceReporter` keeps a weak reference to the provided core. + /// + /// - Parameter core: The core instance. + init(core: DatadogCoreProtocol) { + self.core = core + } + + func generateBacktrace(threadID: ThreadID) throws -> BacktraceReport? { + guard let core = core else { + return nil + } + + guard let backtraceFeature = core.get(feature: BacktraceReportingFeature.self) else { + DD.logger.warn( + """ + Backtrace will not be generated as this capability is not available. + Enable `DatadogCrashReporting` to leverage backtrace generation. + """ + ) + return nil + } + return try backtraceFeature.reporter.generateBacktrace(threadID: threadID) + } +} + +/// Adds capability of reporting backtraces. +extension DatadogCoreProtocol { + /// Registers backtrace reporter in Core. + /// - Parameter backtraceReporter: the implementation of backtrace reporter. + public func register(backtraceReporter: BacktraceReporting) throws { + guard get(feature: BacktraceReportingFeature.self) == nil else { + DD.logger.debug("Backtrace reporter is already registered to this core. Skipping registration of next one.") + return + } + + let feature = BacktraceReportingFeature(reporter: backtraceReporter) + try register(feature: feature) + } + + /// Backtrace reporter. Use it to snapshot all running threads in the current process. + /// + /// It requires `BacktraceReportingFeature` registered to Datadog core. Otherwise reported backtraces will be `nil`. + public var backtraceReporter: BacktraceReporting { CoreBacktraceReporter(core: self) } +} diff --git a/DatadogInternal/Sources/BacktraceReporting/BacktraceReportingFeature.swift b/DatadogInternal/Sources/BacktraceReporting/BacktraceReportingFeature.swift new file mode 100644 index 0000000000..2de09b2b52 --- /dev/null +++ b/DatadogInternal/Sources/BacktraceReporting/BacktraceReportingFeature.swift @@ -0,0 +1,22 @@ +/* + * 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 + +internal final class BacktraceReportingFeature: DatadogFeature { + static var name: String = "backtrace-reporting" + + let messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() + + /// A type capable of generating backtrace reports. + let reporter: BacktraceReporting + + /// Creates `BacktraceReportingFeature`. + /// - Parameter reporter: An external implementation of a type capable of generating backtrace reports. + init(reporter: BacktraceReporting) { + self.reporter = reporter + } +} 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/Codable/AnyCodable.swift b/DatadogInternal/Sources/Codable/AnyCodable.swift new file mode 100644 index 0000000000..4720fd6f6a --- /dev/null +++ b/DatadogInternal/Sources/Codable/AnyCodable.swift @@ -0,0 +1,98 @@ +/* + * 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. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +#if canImport(Foundation) +import Foundation +#endif + +/** + A type-erased `Codable` value. + The `AnyCodable` type forwards encoding and decoding responsibilities + to an underlying value, hiding its specific underlying type. + You can encode or decode mixed-type values in dictionaries + and other collections that require `Encodable` or `Decodable` conformance + by declaring their contained type to be `AnyCodable`. + - SeeAlso: `AnyEncodable` + - SeeAlso: `AnyDecodable` + */ +@frozen +public struct AnyCodable: Codable { + public let value: Any + + public init(_ value: T?) { + self.value = value ?? () + } +} + +extension AnyCodable: _AnyEncodable, _AnyDecodable {} + +extension AnyCodable: Equatable { + public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { + switch (lhs.value, rhs.value) { + case is (Void, Void): + return true + case let (lhs as Bool, rhs as Bool): + return lhs == rhs + case let (lhs as Int, rhs as Int): + return lhs == rhs + case let (lhs as Int8, rhs as Int8): + return lhs == rhs + case let (lhs as Int16, rhs as Int16): + return lhs == rhs + case let (lhs as Int32, rhs as Int32): + return lhs == rhs + case let (lhs as Int64, rhs as Int64): + return lhs == rhs + case let (lhs as UInt, rhs as UInt): + return lhs == rhs + case let (lhs as UInt8, rhs as UInt8): + return lhs == rhs + case let (lhs as UInt16, rhs as UInt16): + return lhs == rhs + case let (lhs as UInt32, rhs as UInt32): + return lhs == rhs + case let (lhs as UInt64, rhs as UInt64): + return lhs == rhs + case let (lhs as Float, rhs as Float): + return lhs == rhs + case let (lhs as Double, rhs as Double): + return lhs == rhs + case let (lhs as String, rhs as String): + return lhs == rhs + case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]): + return lhs == rhs + case let (lhs as [AnyCodable], rhs as [AnyCodable]): + return lhs == rhs + case let (lhs as [String: Any], rhs as [String: Any]): + return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs) + case let (lhs as [Any], rhs as [Any]): + return NSArray(array: lhs) == NSArray(array: rhs) + case is (NSNull, NSNull): + return true + default: + return false + } + } +} diff --git a/DatadogInternal/Sources/Codable/AnyDecodable.swift b/DatadogInternal/Sources/Codable/AnyDecodable.swift new file mode 100644 index 0000000000..ef851e58f9 --- /dev/null +++ b/DatadogInternal/Sources/Codable/AnyDecodable.swift @@ -0,0 +1,151 @@ +/* + * 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. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +#if canImport(Foundation) +import Foundation +#endif + +/** + A type-erased `Decodable` value. + The `AnyDecodable` type forwards decoding responsibilities + to an underlying value, hiding its specific underlying type. + You can decode mixed-type values in dictionaries + and other collections that require `Decodable` conformance + by declaring their contained type to be `AnyDecodable`: + let json = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "null": null + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json) + */ +@frozen +public struct AnyDecodable: Decodable { + public let value: Any + + public init(_ value: T?) { + self.value = value ?? () + } +} + +@usableFromInline +internal protocol _AnyDecodable { + var value: Any { get } + init(_ value: T?) +} + +extension AnyDecodable: _AnyDecodable {} + +extension _AnyDecodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + #if canImport(Foundation) + self.init(NSNull()) + #else + self.init(Optional.none) + #endif + } else if let bool = try? container.decode(Bool.self) { + self.init(bool) + } else if let int = try? container.decode(Int.self) { + self.init(int) + } else if let int = try? container.decode(Int64.self) { + self.init(int) + } else if let uint = try? container.decode(UInt.self) { + self.init(uint) + } else if let uint = try? container.decode(UInt64.self) { + self.init(uint) + } else if let double = try? container.decode(Double.self) { + self.init(double) + } else if let string = try? container.decode(String.self) { + self.init(string) + } else if let passthrough = container.passthrough() { + self.init(passthrough) + } else if let array = try? container.decode([AnyDecodable].self) { + self.init(array.map { $0.value }) + } else if let dictionary = try? container.decode([String: AnyDecodable].self) { + self.init(dictionary.mapValues { $0.value }) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded") + } + } +} + +extension AnyDecodable: Equatable { + public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { + switch (lhs.value, rhs.value) { +#if canImport(Foundation) + case is (NSNull, NSNull), is (Void, Void): + return true +#endif + case let (lhs as Bool, rhs as Bool): + return lhs == rhs + case let (lhs as Int, rhs as Int): + return lhs == rhs + case let (lhs as Int8, rhs as Int8): + return lhs == rhs + case let (lhs as Int16, rhs as Int16): + return lhs == rhs + case let (lhs as Int32, rhs as Int32): + return lhs == rhs + case let (lhs as Int64, rhs as Int64): + return lhs == rhs + case let (lhs as UInt, rhs as UInt): + return lhs == rhs + case let (lhs as UInt8, rhs as UInt8): + return lhs == rhs + case let (lhs as UInt16, rhs as UInt16): + return lhs == rhs + case let (lhs as UInt32, rhs as UInt32): + return lhs == rhs + case let (lhs as UInt64, rhs as UInt64): + return lhs == rhs + case let (lhs as Float, rhs as Float): + return lhs == rhs + case let (lhs as Double, rhs as Double): + return lhs == rhs + case let (lhs as String, rhs as String): + return lhs == rhs + case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]): + return lhs == rhs + case let (lhs as [AnyDecodable], rhs as [AnyDecodable]): + return lhs == rhs + default: + return false + } + } +} diff --git a/DatadogInternal/Sources/Codable/AnyDecoder.swift b/DatadogInternal/Sources/Codable/AnyDecoder.swift new file mode 100644 index 0000000000..1204fdd3a2 --- /dev/null +++ b/DatadogInternal/Sources/Codable/AnyDecoder.swift @@ -0,0 +1,874 @@ +/* + * 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 + +/// An object that decodes instances of a `Any` type. +/// +/// The example below shows how to decode an instance of a simple `GroceryProduct` +/// type from a `Any` object. The type adopts `Codable` so that it's decodable using a +/// `AnyDecoder` instance. +/// +/// struct GroceryProduct: Codable { +/// var name: String +/// var points: Int +/// var description: String? +/// } +/// +/// let dictionary: [String: Any] = [ +/// "name": "Durian", +/// "points": 600, +/// "description": "A fruit with a distinctive scent." +/// ] +/// +/// let decoder = AnyDecoder() +/// let product = try decoder.decode(GroceryProduct.self, from: dictionary) +/// +/// print(product.name) // Prints "Durian" +/// +open class AnyDecoder { + /// Initializes `self`. + public init() { } + + /// Decodes a top-level any value to the given type. + /// + /// - parameter type: The type of the value to decode. + /// - parameter object: The object to decode from. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON. + /// - throws: An error if any value throws an error during decoding. + open func decode(_ type: T.Type = T.self, from any: Any?) throws -> T where T: Decodable { + // swiftlint:disable:previous function_default_parameter_at_end + let container = _AnyDecoder.SingleValueContainer(any) + return try container.decode(T.self) + } +} + +// swiftlint:enable closing_brace_whitespace +// MARK: - Internal Decoder +private class _AnyDecoder: Decoder { + /// The path of coding keys taken to get to this point in decoding. + let codingPath: [CodingKey] + + /// The source value. + let value: Any? + + /// Any contextual information set by the user for decoding. + let userInfo: [CodingUserInfoKey: Any] = [:] + + init(_ value: Any?, path: [CodingKey] = []) { + self.value = value + codingPath = path + } + + /// Returns the data stored in this decoder as represented in a container + /// keyed by the given key type. + /// + /// - parameter type: The key type to use for the container. + /// - returns: A keyed decoding container view into this decoder. + /// - throws: `DecodingError.typeMismatch` if the encountered stored value is + /// not a keyed container. + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { + let container = try KeyedContainer(value, path: codingPath) + return KeyedDecodingContainer(container) + } + + /// Returns the data stored in this decoder as represented in a container + /// appropriate for holding values with no keys. + /// + /// - returns: An unkeyed container view into this decoder. + /// - throws: `DecodingError.typeMismatch` if the encountered stored value is + /// not an unkeyed container. + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + try UnkeyedContainer(value, path: codingPath) + } + + /// Returns the data stored in this decoder as represented in a container + /// appropriate for holding a single primitive value. + /// + /// - returns: A single value container view into this decoder. + /// - throws: `DecodingError.typeMismatch` if the encountered stored value is + /// not a single value container. + func singleValueContainer() throws -> SingleValueDecodingContainer { + SingleValueContainer(value, path: codingPath) + } + + /// A type that provides a view into an encoder's storage and is used to hold + /// the encoded properties of an encodable type in a keyed manner. + struct KeyedContainer: KeyedDecodingContainerProtocol where Key: CodingKey { + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + /// The source dictionary. + let dict: [String: Any?] + + init(_ any: Any?, path: [CodingKey] = []) throws { + guard let dict = any as? [String: Any?] else { + let context = DecodingError.Context( + codingPath: path, + debugDescription: "Invalid conversion of '\(String(describing: any))' to Dictionary." + ) + + throw DecodingError.typeMismatch([String: Any?].self, context) + } + + self.dict = dict + self.codingPath = path + } + + /// All the keys the `Decoder` has for this container. + /// + /// Different keyed containers from the same `Decoder` may return different + /// keys here; it is possible to encode with multiple key types which are + /// not convertible to one another. This should report all keys present + /// which are convertible to the requested type. + var allKeys: [Key] { + dict.keys.compactMap { Key(stringValue: $0) } + } + + /// Returns a Boolean value indicating whether the decoder contains a value + /// associated with the given key. + /// + /// The value associated with `key` may be a null value as appropriate for + /// the data format. + /// + /// - parameter key: The key to search for. + /// - returns: Whether the `Decoder` has an entry for the given key. + func contains(_ key: Key) -> Bool { + dict[key.stringValue] != nil + } + + func value(forKey key: Key) throws -> Any? { + if let value = dict[key.stringValue] { + return value + } + + let context = DecodingError.Context( + codingPath: codingPath + [key], + debugDescription: "No value associated with key \(key.stringValue)." + ) + + throw DecodingError.keyNotFound(key, context) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decodeNil(forKey key: Key) throws -> Bool { + try nestedSingleValueContainer(forKey: key).decodeNil() + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: String.Type, forKey key: Key) throws -> String { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { + try nestedSingleValueContainer(forKey: key).decode(type) + } + + /// Returns the data stored for the given key as represented in a container + /// keyed by the given key type. + /// + /// - parameter type: The key type to use for the container. + /// - parameter key: The key that the nested container is associated with. + /// - returns: A keyed decoding container view into `self`. + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + let container = try KeyedContainer(value(forKey: key), path: codingPath + [key]) + return KeyedDecodingContainer(container) + } + + /// Returns the data stored for the given key as represented in an unkeyed + /// container. + /// + /// - parameter key: The key that the nested container is associated with. + /// - returns: An unkeyed decoding container view into `self`. + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + try UnkeyedContainer(value(forKey: key), path: codingPath + [key]) + } + + /// Returns the data stored for the given key as represented in an single value + /// container. + /// + /// - parameter key: The key that the nested container is associated with. + /// - returns: An single value decoding container view into `self`. + func nestedSingleValueContainer(forKey key: Key) throws -> SingleValueDecodingContainer { + try SingleValueContainer(value(forKey: key), path: codingPath + [key]) + } + + /// Returns a `Decoder` instance for decoding `super` from the container + /// associated with the default `super` key. + /// + /// Equivalent to calling `superDecoder(forKey:)` with + /// `Key(stringValue: "super", intValue: 0)`. + /// + /// - returns: A new `Decoder` to pass to `super.init(from:)`. + func superDecoder() throws -> Decoder { + _AnyDecoder(dict, path: codingPath) + } + + /// Returns a `Decoder` instance for decoding `super` from the container + /// associated with the given key. + /// + /// - parameter key: The key to decode `super` for. + /// - returns: A new `Decoder` to pass to `super.init(from:)`. + func superDecoder(forKey key: Key) throws -> Decoder { + try _AnyDecoder(value(forKey: key), path: codingPath + [key]) + } + } + + /// A type that provides a view into a decoder's storage and is used to hold + /// the encoded properties of a decodable type sequentially, without keys. + struct UnkeyedContainer: UnkeyedDecodingContainer { + /// The path of coding keys taken to get to this point in decoding. + let codingPath: [CodingKey] + + /// The number of elements contained within this container. + /// + /// If the number of elements is unknown, the value is `nil`. + var count: Int? { array.count } + + /// A Boolean value indicating whether there are no more elements left to be + /// decoded in the container. + var isAtEnd: Bool { currentIndex >= array.count } + + /// The current decoding index of the container (i.e. the index of the next + /// element to be decoded.) Incremented after every successful decode call. + private(set) var currentIndex: Int = 0 + + /// The source array. + private let array: [Any?] + + init(_ any: Any?, path: [CodingKey] = []) throws { + guard let array = any as? [Any?] else { + let context = DecodingError.Context( + codingPath: path, + debugDescription: "Invalid conversion of '\(String(describing: any))' to Array." + ) + + throw DecodingError.typeMismatch([Any?].self, context) + } + + self.array = array + self.codingPath = path + } + + /// Decodes a null value. + /// + /// If the value is not null, does not increment currentIndex. + /// + /// - returns: Whether the encountered value was null. + mutating func decodeNil() throws -> Bool { + if try nestedSingleValueContainer().decodeNil() { + return true + } + + currentIndex -= 1 + return false + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Bool.Type) throws -> Bool { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: String.Type) throws -> String { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Double.Type) throws -> Double { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Float.Type) throws -> Float { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Int.Type) throws -> Int { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Int8.Type) throws -> Int8 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Int16.Type) throws -> Int16 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Int32.Type) throws -> Int32 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: Int64.Type) throws -> Int64 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: UInt.Type) throws -> UInt { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + try nestedSingleValueContainer().decode(type) + } + + /// Decodes a value of the given type. + /// + /// - parameter type: The type of value to decode. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + mutating func decode(_ type: T.Type) throws -> T where T: Decodable { + try nestedSingleValueContainer().decode(type) + } + + private mutating func next() throws -> Any? { + defer { currentIndex += 1 } + + if isAtEnd { + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: "Unkeyed container is at end." + ) + + throw DecodingError.valueNotFound(Any.self, context) + } + + return array[currentIndex] + } + + /// Decodes a nested container keyed by the given type. + /// + /// - parameter type: The key type to use for the container. + /// - returns: A keyed decoding container view into `self`. + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey { + let container = try KeyedContainer(next(), path: codingPath) + return KeyedDecodingContainer(container) + } + + /// Decodes an unkeyed nested container. + /// + /// - returns: An unkeyed decoding container view into `self`. + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + try UnkeyedContainer(next(), path: codingPath) + } + + /// Decodes an single value nested container. + /// + /// - returns: An unkeyed decoding container view into `self`. + mutating func nestedSingleValueContainer() throws -> SingleValueDecodingContainer { + try SingleValueContainer(next(), path: codingPath) + } + + /// Decodes a nested container and returns a `Decoder` instance for decoding + /// `super` from that container. + /// + /// - returns: A new `Decoder` to pass to `super.init(from:)`. + mutating func superDecoder() throws -> Decoder { + _AnyDecoder(array, path: codingPath) + } + } + + /// A container that can support the storage and direct decoding of a single + /// nonkeyed value. + struct SingleValueContainer: SingleValueDecodingContainer { + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + let value: Any? + + init(_ value: Any?, path: [CodingKey] = []) { + self.value = value + self.codingPath = path + } + + /// Decodes a null value. + /// + /// - returns: Whether the encountered value was null. + func decodeNil() -> Bool { + value == nil + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Bool.Type) throws -> Bool { + guard let value = value as? Bool else { + throw DecodingError.typeMismatch(type, in: self) + } + return value + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: String.Type) throws -> String { + guard let value = value as? String else { + throw DecodingError.typeMismatch(type, in: self) + } + return value + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Double.Type) throws -> Double { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Float.Type) throws -> Float { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Int.Type) throws -> Int { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Int8.Type) throws -> Int8 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Int16.Type) throws -> Int16 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Int32.Type) throws -> Int32 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: Int64.Type) throws -> Int64 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: UInt.Type) throws -> UInt { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: UInt8.Type) throws -> UInt8 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: UInt16.Type) throws -> UInt16 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: UInt32.Type) throws -> UInt32 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: UInt64.Type) throws -> UInt64 { + try value(as: type) + } + + /// Decodes a single value of the given type. + /// + /// - parameter type: The type to decode as. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + func decode(_ type: T.Type) throws -> T where T: Decodable { + if let value = value as? T { + return value + } + + let decoder = _AnyDecoder(value, path: codingPath) + return try T(from: decoder) + } + + /// Converts value to any `BinaryInteger`. + /// + /// - Parameter type: The `BinaryInteger` to convert as. + /// - Returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + private func value(as type: T.Type) throws -> T where T: BinaryInteger { + var value: T? + switch self.value { + case let source as T: return source + case let source as Int: value = T(exactly: source) + case let source as Int8: value = T(exactly: source) + case let source as Int16: value = T(exactly: source) + case let source as Int32: value = T(exactly: source) + case let source as Int64: value = T(exactly: source) + case let source as UInt: value = T(exactly: source) + case let source as UInt8: value = T(exactly: source) + case let source as UInt16: value = T(exactly: source) + case let source as UInt32: value = T(exactly: source) + case let source as UInt64: value = T(exactly: source) + default: break + } + + guard let value = value else { + throw DecodingError.typeMismatch(type, in: self) + } + + return value + } + + /// Converts value to any `BinaryFloatingPoint`. + /// + /// - Parameter type: The `BinaryFloatingPoint` to convert as. + /// - Returns: A value of the requested type. + /// - throws: `DecodingError.typeMismatch` if the value conversion + /// fails. + private func value(as type: T.Type) throws -> T where T: BinaryFloatingPoint { + if let source = value as? T { + return source + } + + if let source = value as? Double { + return T(source) + } + + if let source = value as? Float { + return T(source) + } + + if let source = try? value(as: Int.self) { + return T(source) + } + + throw DecodingError.typeMismatch(type, in: self) + } + } +} + +private extension DecodingError { + /// Returns a new `.typeMismatch` error using a constructed coding path and + /// the given container. + /// + /// The coding path for the returned error is the given container's coding + /// path. + /// + /// - param container: The container in which the corrupted data was + /// accessed. + /// - param debugDescription: A description of the error to aid in debugging. + /// + /// - Returns: A new `.typeMismatch` error with the given information. + static func typeMismatch(_ type: Any.Type, in container: _AnyDecoder.SingleValueContainer) -> DecodingError { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid conversion of '\(String(describing: container.value))' to \(type)." + ) + + return .typeMismatch(type, context) + } +} + +extension SingleValueDecodingContainer { + /// Decodes a passthrough value of the given type. + /// + /// This method can succeed only when using `AnyDecoder`. + /// + /// - returns: A passthrough value if any. + /// - throws: `DecodingError.typeMismatch` if the value was not passthrough. + func passthrough() -> PassthroughAnyCodable? { + guard let container = self as? _AnyDecoder.SingleValueContainer else { + return nil + } + + return container.value as? PassthroughAnyCodable + } +} diff --git a/DatadogInternal/Sources/Codable/AnyEncodable.swift b/DatadogInternal/Sources/Codable/AnyEncodable.swift new file mode 100644 index 0000000000..995f09e6f7 --- /dev/null +++ b/DatadogInternal/Sources/Codable/AnyEncodable.swift @@ -0,0 +1,213 @@ +/* + * 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. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +#if canImport(Foundation) +import Foundation +#endif + +/** + A type-erased `Encodable` value. + The `AnyEncodable` type forwards encoding responsibilities + to an underlying value, hiding its specific underlying type. + You can encode mixed-type values in dictionaries + and other collections that require `Encodable` conformance + by declaring their contained type to be `AnyEncodable`: + let dictionary: [String: AnyEncodable] = [ + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": [ + "a": "alpha", + "b": "bravo", + "c": "charlie" + ], + "null": nil + ] + let encoder = JSONEncoder() + let json = try! encoder.encode(dictionary) + */ +@frozen +public struct AnyEncodable: Encodable { + public let value: Any + + public init(_ value: T?) { + self.value = value ?? () + } +} + +@usableFromInline +internal protocol _AnyEncodable { + var value: Any { get } + init(_ value: T?) +} + +extension AnyEncodable: _AnyEncodable {} + +// MARK: - Encodable +extension _AnyEncodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch value { + case is Void: + try container.encodeNil() + #if canImport(Foundation) + case is NSNull: + try container.encodeNil() + case let number as NSNumber: + try encode(nsnumber: number, into: &container) + case let date as Date: + try container.encode(date) + case let url as URL: + try container.encode(url.absoluteString) + #else + case let bool as Bool: + try container.encode(bool) + case let int as Int: + try container.encode(int) + case let int8 as Int8: + try container.encode(int8) + case let int16 as Int16: + try container.encode(int16) + case let int32 as Int32: + try container.encode(int32) + case let int64 as Int64: + try container.encode(int64) + case let uint as UInt: + try container.encode(uint) + case let uint8 as UInt8: + try container.encode(uint8) + case let uint16 as UInt16: + try container.encode(uint16) + case let uint32 as UInt32: + try container.encode(uint32) + case let uint64 as UInt64: + try container.encode(uint64) + case let float as Float: + try container.encode(float) + case let double as Double: + try container.encode(double) + #endif + case let string as String: + try container.encode(string) + case let array as [Any?]: + try container.encode(array.map { AnyEncodable($0) }) + case let dictionary as [String: Any?]: + try container.encode(dictionary.mapValues { AnyEncodable($0) }) + case let encodable as Encodable: + try encodable.encode(to: encoder) + default: + let context = EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "AnyEncodable value cannot be encoded: \(type(of: value))" + ) + throw EncodingError.invalidValue(value, context) + } + } + + #if canImport(Foundation) + private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { + // objCType: A C string containing the Objective-C type of the data contained + // in the value object. This property provides the same string produced by + // the @encode() compiler directive. This property is more reliable than + // CFNumberGetType(nsnumber) which can return a wrong type after casting a + // Swift fixed-width numeric. + // + // The list of NSNumber encoding value can be found in the following links: + // - https://developer.apple.com/documentation/foundation/nsnumber + // - https://github.com/gnustep/libs-base/blob/master/Source/NSNumber.m + switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) { + case "c": + try container.encode(nsnumber.boolValue) + case "s": + try container.encode(nsnumber.int16Value) + case "i", "l": + try container.encode(nsnumber.int32Value) + case "q": + try container.encode(nsnumber.int64Value) + case "C": + try container.encode(nsnumber.uint8Value) + case "S": + try container.encode(nsnumber.uint16Value) + case "I", "L": + try container.encode(nsnumber.uint32Value) + case "Q": + try container.encode(nsnumber.uint64Value) + case "f": + try container.encode(nsnumber.floatValue) + case "d": + try container.encode(nsnumber.doubleValue) + default: + let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled") + throw EncodingError.invalidValue(nsnumber, context) + } + } + #endif +} + +extension AnyEncodable: Equatable { + public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool { + switch (lhs.value, rhs.value) { + case is (Void, Void): + return true + case let (lhs as Bool, rhs as Bool): + return lhs == rhs + case let (lhs as Int, rhs as Int): + return lhs == rhs + case let (lhs as Int8, rhs as Int8): + return lhs == rhs + case let (lhs as Int16, rhs as Int16): + return lhs == rhs + case let (lhs as Int32, rhs as Int32): + return lhs == rhs + case let (lhs as Int64, rhs as Int64): + return lhs == rhs + case let (lhs as UInt, rhs as UInt): + return lhs == rhs + case let (lhs as UInt8, rhs as UInt8): + return lhs == rhs + case let (lhs as UInt16, rhs as UInt16): + return lhs == rhs + case let (lhs as UInt32, rhs as UInt32): + return lhs == rhs + case let (lhs as UInt64, rhs as UInt64): + return lhs == rhs + case let (lhs as Float, rhs as Float): + return lhs == rhs + case let (lhs as Double, rhs as Double): + return lhs == rhs + case let (lhs as String, rhs as String): + return lhs == rhs + case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]): + return lhs == rhs + case let (lhs as [AnyEncodable], rhs as [AnyEncodable]): + return lhs == rhs + default: + return false + } + } +} diff --git a/DatadogInternal/Sources/Codable/AnyEncoder.swift b/DatadogInternal/Sources/Codable/AnyEncoder.swift new file mode 100644 index 0000000000..8cb0e61e18 --- /dev/null +++ b/DatadogInternal/Sources/Codable/AnyEncoder.swift @@ -0,0 +1,678 @@ +/* + * 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 + +/// An object that encodes instances of an `Encodable` type as `Any?`. +/// +/// The example below shows how to encode an instance of a simple `GroceryProduct` +/// type to `Any` object. The type adopts `Codable` so that it's encodable as `Any` +/// using a `AnyEncoder` instance. +/// +/// struct GroceryProduct: Codable { +/// var name: String +/// var points: Int +/// var description: String? +/// } +/// +/// let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.") +/// +/// let encoder = AnyEncoder() +/// +/// let object = try encoder.encode(pear) +/// print(object as! NSDictionary) +/// +/// /* Prints: +/// { +/// description = "A ripe pear."; +/// name = Pear; +/// points = 250; +/// } +/// */ +open class AnyEncoder { + /// Initializes `self`. + public init() { } + + /// Encodes the given top-level value and returns its Any representation. + /// + /// Depending on the value and its `Encodable` implementation the returned + /// encoded value can be `Any`, `[Any?]`, `[String: Any?]`, or `nil`. + /// + /// - parameter value: The value to encode. + /// - returns: An `Any` object containing the value. + /// - throws: An error if any value throws an error during encoding. + open func encode(_ value: T) throws -> Any? where T: Encodable { + let encoder = _AnyEncoder() + try value.encode(to: encoder) + return encoder.any + } +} + +/// A type that can encode values into a native format for external +/// representation. +private class _AnyEncoder: Encoder { + typealias AnyEncodingStorage = (Any?) -> Void + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + /// Any contextual information set by the user for encoding. + let userInfo: [CodingUserInfoKey: Any] = [:] + + /// The encoded value. + var any: Any? + + init(path: [CodingKey] = []) { + codingPath = path + } + + /// Returns an encoding container appropriate for holding multiple values + /// keyed by the given key type. + /// + /// You must use only one kind of top-level encoding container. This method + /// must not be called after a call to `unkeyedContainer()` or after + /// encoding a value through a call to `singleValueContainer()` + /// + /// - parameter type: The key type to use for the container. + /// - returns: A new keyed encoding container. + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + let container = KeyedContainer( + store: { self.any = $0 }, + dictionary: any as? [String: Any?], + path: codingPath + ) + self.any = container.dictionary + return KeyedEncodingContainer(container) + } + + /// Returns an encoding container appropriate for holding multiple unkeyed + /// values. + /// + /// You must use only one kind of top-level encoding container. This method + /// must not be called after a call to `container(keyedBy:)` or after + /// encoding a value through a call to `singleValueContainer()` + /// + /// - returns: A new empty unkeyed container. + func unkeyedContainer() -> UnkeyedEncodingContainer { + let container = UnkeyedContainer( + store: { self.any = $0 }, + array: any as? [Any?], + path: codingPath + ) + self.any = container.array + return container + } + + /// Returns an encoding container appropriate for holding a single primitive + /// value. + /// + /// You must use only one kind of top-level encoding container. This method + /// must not be called after a call to `unkeyedContainer()` or + /// `container(keyedBy:)`, or after encoding a value through a call to + /// `singleValueContainer()` + /// + /// - returns: A new empty single value container. + func singleValueContainer() -> SingleValueEncodingContainer { + SingleValueContainer( + store: { self.any = $0 }, + path: codingPath + ) + } + + /// A concrete container that provides a view into an encoder's storage, making + /// the encoded properties of an encodable type accessible by keys. + class KeyedContainer: KeyedEncodingContainerProtocol where Key: CodingKey { + /// The path of coding keys taken to get to this point in encoding. + var codingPath: [CodingKey] + + /// The dictionary of encoded value. + var dictionary: [String: Any?] + + /// The storage closure to call with encoded value. + let store: AnyEncodingStorage + + /// Creates a keyed container for encoding an `Encodable` object to + /// a dictionary of `[String: Any?]`. + /// + /// - Parameters: + /// - store: The storage closure to call with encoded value. + /// - dictionary: An existing dictionary of any. + /// - path: The path of coding keys taken to get to this point in encoding. + init( + store: @escaping AnyEncodingStorage, + dictionary: [String: Any?]? = nil, + path: [CodingKey] = [] + ) { + self.store = store + self.dictionary = dictionary ?? [:] + self.codingPath = path + } + + /// Encodes a null value for the given key. + /// + /// - parameter key: The key to associate the value with. + func encodeNil(forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encodeNil() + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Bool, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: String, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Double, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Float, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Int, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Int8, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Int16, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Int32, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: Int64, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: UInt, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: UInt8, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: UInt16, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: UInt32, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: UInt64, forKey key: Key) throws { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + func encode(_ value: T, forKey key: Key) throws where T: Encodable { + try nestedSingleValueContainer(forKey: key).encode(value) + } + + /// Stores a keyed encoding container for the given key and returns it. + /// + /// - parameter keyType: The key type to use for the container. + /// - parameter key: The key to encode the container for. + /// - returns: A new keyed encoding container. + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey: CodingKey { + let container = KeyedContainer( + store: { self.set($0, forKey: key) }, + path: codingPath + [key] + ) + return KeyedEncodingContainer(container) + } + + /// Stores an unkeyed encoding container for the given key and returns it. + /// + /// - parameter key: The key to encode the container for. + /// - returns: A new unkeyed encoding container. + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + UnkeyedContainer( + store: { self.set($0, forKey: key) }, + path: codingPath + [key] + ) + } + + /// Stores an single value encoding container for the given key and returns it. + /// + /// - parameter key: The key to encode the container for. + /// - returns: A new unkeyed encoding container. + func nestedSingleValueContainer(forKey key: Key) -> SingleValueContainer { + SingleValueContainer( + store: { self.set($0, forKey: key) }, + path: codingPath + [key] + ) + } + + /// Set the encoded value at the given key. + /// + /// - Parameters: + /// - any: The encoded value. + /// - key: The key to encode the value for. + private func set(_ any: Any?, forKey key: Key) { + dictionary[key.stringValue] = any + store(dictionary) + } + + /// Stores a new nested container for the default `super` key and returns a + /// new encoder instance for encoding `super` into that container. + /// + /// Equivalent to calling `superEncoder(forKey:)` with + /// `Key(stringValue: "super", intValue: 0)`. + /// + /// - returns: A new encoder to pass to `super.encode(to:)`. + func superEncoder() -> Encoder { + _AnyEncoder(path: codingPath) + } + + /// Stores a new nested container for the given key and returns a new encoder + /// instance for encoding `super` into that container. + /// + /// - parameter key: The key to encode `super` for. + /// - returns: A new encoder to pass to `super.encode(to:)`. + func superEncoder(forKey key: Key) -> Encoder { + _AnyEncoder(path: codingPath + [key]) + } + } + + /// A type that provides a view into an encoder's storage and is used to hold + /// the encoded properties of an encodable type sequentially, without keys. + class UnkeyedContainer: UnkeyedEncodingContainer { + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + /// The number of elements encoded into the container. + var count: Int { array.count } + + /// The array of encoded value. + var array: [Any?] + + /// The storage closure to call with encoded value. + let store: AnyEncodingStorage + + /// Creates a unkeyed container for encoding an `Encodable` object to + /// an array of `[Any?]`. + /// + /// - Parameters: + /// - store: The storage closure to call with encoded value. + /// - array: An existing array of any. + /// - path: The path of coding keys taken to get to this point in encoding. + init( + store: @escaping AnyEncodingStorage, + array: [Any?]? = nil, + path: [CodingKey] = [] + ) { + self.store = store + self.array = array ?? [] + self.codingPath = path + } + + /// Encodes a null value. + func encodeNil() throws { + try nestedSingleValueContainer().encodeNil() + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Bool) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: String) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Double) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Float) throws { + try nestedSingleValueContainer().encode(value) + } + + func encode(_ value: Int) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int8) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int16) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int32) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int64) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt8) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt16) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt32) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt64) throws { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + func encode(_ value: T) throws where T: Encodable { + try nestedSingleValueContainer().encode(value) + } + + /// Encodes a nested container keyed by the given type and returns it. + /// + /// - parameter keyType: The key type to use for the container. + /// - returns: A new keyed encoding container. + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey: CodingKey { + let container = KeyedContainer(store: append, path: codingPath) + return KeyedEncodingContainer(container) + } + + /// Encodes an unkeyed encoding container and returns it. + /// + /// - returns: A new unkeyed encoding container. + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + UnkeyedContainer(store: append, path: codingPath) + } + + /// Encodes an single value encoding container and returns it. + /// + /// - returns: A new unkeyed encoding container. + func nestedSingleValueContainer() -> SingleValueContainer { + SingleValueContainer(store: append, path: codingPath) + } + + /// Encodes a nested container and returns an `Encoder` instance for encoding + /// `super` into that container. + /// + /// - returns: A new encoder to pass to `super.encode(to:)`. + func superEncoder() -> Encoder { + _AnyEncoder(path: codingPath) + } + + private func append(_ any: Any?) { + array.append(any) + store(array) + } + } + + /// A container that can support the storage and direct encoding of a single + /// non-keyed value. + struct SingleValueContainer: SingleValueEncodingContainer { + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + /// The storage closure to call with encoded value. + let store: AnyEncodingStorage + + /// Creates a single value container for encoding an `Encodable` object to + /// `Any?`. + /// + /// - Parameters: + /// - store: The storage closure to call with encoded value. + /// - path: The path of coding keys taken to get to this point in encoding. + init( + store: @escaping AnyEncodingStorage, + path: [CodingKey] = [] + ) { + self.store = store + self.codingPath = path + } + + /// Encodes a null value. + func encodeNil() throws { + store(nil) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Bool) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: String) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Double) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Float) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int8) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int16) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int32) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: Int64) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt8) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt16) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt32) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: UInt64) throws { + store(value) + } + + /// Encodes a single value of the given type. + /// + /// - parameter value: The value to encode. + func encode(_ value: T) throws where T: Encodable { + if value is PassthroughAnyCodable { + store(value) + } else { + let encoder = _AnyEncoder(path: codingPath) + try value.encode(to: encoder) + store(encoder.any) + } + } + } +} + +/// A passthrough object will skip encoding when using the ``AnyEncoder``. +/// The object will be stored as-is in the returned `Any?` container. +/// +/// Making an `Encodable` as passthrough allow to bypass encoding when the type is +/// known by multiple parties. +/// +/// When decoding an object using the ``AnyDecoder``, the decoder will +/// attempt to cast the object to the expected type, a `DecodingError.typeMismatch` +/// error is raised in case of failure. +public protocol PassthroughAnyCodable { } + +extension URL: PassthroughAnyCodable { } +extension Date: PassthroughAnyCodable { } +extension UUID: PassthroughAnyCodable { } +extension Data: PassthroughAnyCodable { } diff --git a/DatadogInternal/Sources/Codable/DynamicCodingKey.swift b/DatadogInternal/Sources/Codable/DynamicCodingKey.swift new file mode 100644 index 0000000000..0cecfede26 --- /dev/null +++ b/DatadogInternal/Sources/Codable/DynamicCodingKey.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 + +public struct DynamicCodingKey: CodingKey, Hashable { + /// The string to use in a named collection (e.g. a string-keyed dictionary). + public var stringValue: String + + /// Creates a new instance from the given string. + /// + /// If the string passed as `stringValue` does not correspond to any instance + /// of this type, the result is `nil`. + /// + /// - parameter stringValue: The string value of the desired key. + public init?(stringValue: String) { + self.stringValue = stringValue + } + + /// The value to use in an integer-indexed collection (e.g. an int-keyed + /// dictionary). + public var intValue: Int? + + /// Creates a new instance from the specified integer. + /// + /// If the value passed as `intValue` does not correspond to any instance of + /// this type, the result is `nil`. + /// + /// - parameter intValue: The integer value of the desired key. + public init?(intValue: Int) { + return nil + } + + /// Creates a new instance from the given string. + /// + /// - parameter stringValue: The string value of the desired key. + public init(_ stringValue: String) { + self.stringValue = stringValue + } +} + +extension DynamicCodingKey: ExpressibleByStringLiteral { + /// Creates an instance initialized to the given string value. + /// + /// - Parameter value: The value of the new instance. + public init(stringLiteral value: String) { + self.init(value) + } +} diff --git a/DatadogInternal/Sources/Concurrency/Flushable.swift b/DatadogInternal/Sources/Concurrency/Flushable.swift new file mode 100644 index 0000000000..5dada3717d --- /dev/null +++ b/DatadogInternal/Sources/Concurrency/Flushable.swift @@ -0,0 +1,14 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +public protocol Flushable { + /// Awaits completion of all asynchronous operations. + /// + /// **blocks the caller thread** + func flush() +} diff --git a/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift b/DatadogInternal/Sources/Concurrency/ReadWriteLock.swift new file mode 100644 index 0000000000..78586b83c5 --- /dev/null +++ b/DatadogInternal/Sources/Concurrency/ReadWriteLock.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 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 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 { + /// The wrapped value. + private var value: Value + + /// The lock object. + private var rwlock = pthread_rwlock_t() + + public init(wrappedValue value: Value) { + pthread_rwlock_init(&rwlock, nil) + self.value = value + } + + deinit { + pthread_rwlock_destroy(&rwlock) + } + + /// The wrapped value. + /// + /// The `get` will acquire the lock for reading while the `set` will acquire for + /// writing. + public var wrappedValue: Value { + get { + pthread_rwlock_rdlock(&rwlock) + defer { pthread_rwlock_unlock(&rwlock) } + return value + } + set { mutate { $0 = newValue } } + } + + /// Provides a non-escaping closure for mutation. + /// The lock will be acquired once for writing before invoking the closure. + /// + /// - Parameter closure: The closure with the mutable value. + public func mutate(_ closure: (inout Value) throws -> Void) rethrows { + pthread_rwlock_wrlock(&rwlock) + defer { pthread_rwlock_unlock(&rwlock) } + try closure(&value) + } +} diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift new file mode 100644 index 0000000000..271d257eab --- /dev/null +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -0,0 +1,216 @@ +/* + * 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 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. + case active + /// The app is running in the foreground but is not receiving events. + /// This might happen as a result of an interruption or because the app is transitioning to or from the background. + 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, .terminated: + return false + } + } +} + +/// A data structure to represent recorded app states in a given period of time +public struct AppStateHistory: Codable, Equatable, PassthroughAnyCodable { + /// Snapshot of the app state at `date` + public struct Snapshot: Codable, Equatable { + /// The app state at this `date`. + public let state: AppState + /// Date of recording this snapshot. + public let date: Date + + public init(state: AppState, date: Date) { + self.state = state + self.date = date + } + } + + public private(set) var initialSnapshot: Snapshot + public private(set) var snapshots: [Snapshot] + + /// Date of the last update to `AppStateHistory`. + public private(set) var recentDate: Date + + /// The most recent app state `Snapshot`. + public var currentSnapshot: Snapshot { + return Snapshot( + state: (snapshots.last ?? initialSnapshot).state, + date: recentDate + ) + } + + public init( + initialSnapshot: Snapshot, + recentDate: Date, + snapshots: [Snapshot] = [] + ) { + self.initialSnapshot = initialSnapshot + self.snapshots = snapshots + self.recentDate = recentDate + } + + public init( + initialState: AppState, + date: Date, + snapshots: [Snapshot] = [] + ) { + self.init( + initialSnapshot: .init(state: initialState, date: date), + recentDate: date, + snapshots: snapshots + ) + } + + /// Limits or extrapolates app state history to the given range + /// This is useful when you record between 0...3t but you are concerned of t...2t only + /// - Parameter range: if outside of initial and final states, it extrapolates; otherwise it limits + /// - Returns: a history instance spanning the given range + public func take(between range: ClosedRange) -> AppStateHistory { + .init( + // move initial state to lowerBound + initialSnapshot: Snapshot( + state: state(at: range.lowerBound), + date: range.lowerBound + ), + // move final state to upperBound + recentDate: range.upperBound, + // filter changes outside of the range + snapshots: snapshots.filter { range.contains($0.date) } + ) + } + + public mutating func append(_ snapshot: Snapshot) { + snapshots.append(snapshot) + } + + public var foregroundDuration: TimeInterval { + var duration: TimeInterval = 0.0 + var lastActiveStartDate: Date? + let allEvents = [initialSnapshot] + snapshots + [currentSnapshot] + for event in allEvents { + if let startDate = lastActiveStartDate { + duration += event.date.timeIntervalSince(startDate) + } + if event.state.isRunningInForeground { + lastActiveStartDate = event.date + } else { + lastActiveStartDate = nil + } + } + return duration + } + + private func state(at date: Date) -> AppState { + for snapshot in snapshots.reversed() { + if snapshot.date > date { + continue + } + + return snapshot.state + } + + // we assume there was no change before initial state + return initialSnapshot.state + } +} + +extension AppStateHistory { + /// Return a history with an active initial state. + /// + /// - Parameter date: The date since the application is considred active. + public static func active(since date: Date) -> AppStateHistory { + .init(initialState: .active, date: date) + } +} + +#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, 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/BatteryStatus.swift b/DatadogInternal/Sources/Context/BatteryStatus.swift new file mode 100644 index 0000000000..a26e4592de --- /dev/null +++ b/DatadogInternal/Sources/Context/BatteryStatus.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 Foundation + +/// Describe the battery state for mobile devices. +public struct BatteryStatus: Codable, Equatable, PassthroughAnyCodable { + public enum State: Codable { + case unknown + case unplugged + case charging + case full + } + + /// The charging state of the battery. + public let state: State + + /// The battery power level, range between 0 and 1. + public let level: Float + + public init(state: State, level: Float) { + self.state = state + self.level = level + } +} diff --git a/DatadogInternal/Sources/Context/BundleType.swift b/DatadogInternal/Sources/Context/BundleType.swift new file mode 100644 index 0000000000..5ab6a16a1d --- /dev/null +++ b/DatadogInternal/Sources/Context/BundleType.swift @@ -0,0 +1,18 @@ +/* + * 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 + +public enum BundleType: String { + /// An iOS application. + case iOSApp + /// An iOS app extension. + case iOSAppExtension + + public init(bundle: Bundle) { + self = bundle.bundlePath.hasSuffix(".appex") ? .iOSAppExtension : .iOSApp + } +} diff --git a/DatadogInternal/Sources/Context/CarrierInfo.swift b/DatadogInternal/Sources/Context/CarrierInfo.swift new file mode 100644 index 0000000000..9e6164f7f0 --- /dev/null +++ b/DatadogInternal/Sources/Context/CarrierInfo.swift @@ -0,0 +1,48 @@ +/* + * 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 + +/// Carrier details specific to cellular radio access. +public struct CarrierInfo: Codable, Equatable, PassthroughAnyCodable { + // swiftlint:disable identifier_name + public enum RadioAccessTechnology: String, Codable, CaseIterable { + case GPRS + case Edge + case WCDMA + case HSDPA + case HSUPA + case CDMA1x + case CDMAEVDORev0 + case CDMAEVDORevA + case CDMAEVDORevB + case eHRPD + case LTE + case unknown + } + // swiftlint:enable identifier_name + + /// The name of the user’s home cellular service provider. + public let carrierName: String? + /// The ISO country code for the user’s cellular service provider. + public let carrierISOCountryCode: String? + /// Indicates if the carrier allows making VoIP calls on its network. + public let carrierAllowsVOIP: Bool + /// The radio access technology used for cellular connection. + public let radioAccessTechnology: RadioAccessTechnology + + public init( + carrierName: String?, + carrierISOCountryCode: String?, + carrierAllowsVOIP: Bool, + radioAccessTechnology: RadioAccessTechnology + ) { + self.carrierName = carrierName + self.carrierISOCountryCode = carrierISOCountryCode + self.carrierAllowsVOIP = carrierAllowsVOIP + self.radioAccessTechnology = radioAccessTechnology + } +} diff --git a/DatadogInternal/Sources/Context/DatadogContext.swift b/DatadogInternal/Sources/Context/DatadogContext.swift new file mode 100644 index 0000000000..daabc0323d --- /dev/null +++ b/DatadogInternal/Sources/Context/DatadogContext.swift @@ -0,0 +1,174 @@ +/* + * 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 + +public struct DatadogContext { + // MARK: - Datadog Specific + + /// [Datadog Site](https://docs.datadoghq.com/getting_started/site/) for data uploads. It can be `nil` in V1 + /// if the SDK is configured using deprecated APIs: + /// `set(logsEndpoint:)`, `set(tracesEndpoint:)` and `set(rumEndpoint:)`. + public let site: DatadogSite + + /// The client token allowing for data uploads to [Datadog Site](https://docs.datadoghq.com/getting_started/site/). + public let clientToken: String + + /// The name of the service that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + public let service: String + + /// The name of the environment that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + public let env: String + + /// The version of the application that data is generated from. Used for [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + public var version: String + + /// The build number of the application that data is generated from. + public let buildNumber: String + + /// The id of the build, specifically for cross platform frameworks + public let buildId: String? + + /// The variant of the build, equivelent to Android's "Flavor". Only used by cross platform SDKs + public let variant: String? + + /// Denotes the mobile application's platform, such as `"ios"` or `"flutter"` that data is generated from. + /// - See: Datadog [Reserved Attributes](https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#reserved-attributes). + public let source: String + + /// Denotes the source type for crashes. This is used for platforms that provide additional symbolication steps for native crashes. + public let nativeSourceOverride: String? + + /// The version of Datadog iOS SDK. + public let sdkVersion: String + + /// The name of [CI Visibility](https://docs.datadoghq.com/continuous_integration/) origin. + /// It is only set if the SDK is running with a context passed from [Swift Tests](https://docs.datadoghq.com/continuous_integration/setup_tests/swift/?tab=swiftpackagemanager) library. + public let ciAppOrigin: String? + + /// Interval between device and server time. + /// + /// The value can change as the device continue to sync with the server. + public var serverTimeOffset: TimeInterval = .zero + + // MARK: - Application Specific + + /// The name of the application, read from `Info.plist` (`CFBundleExecutable`). + public let applicationName: String + + /// The bundle identifier, read from `Info.plist` (`CFBundleIdentifier`). + public let applicationBundleIdentifier: String + + /// The type of the bundle running the SDK. + public let applicationBundleType: BundleType + + /// Date of SDK initialization measured in device time (without NTP correction). + public let sdkInitDate: Date + + /// Current device information. + public var device: DeviceInfo + + /// Current user information. + public var userInfo: UserInfo? + + /// The user's consent to data collection + public var trackingConsent: TrackingConsent = .pending + + /// Application launch time. + /// + /// Can be `nil` if the launch could not yet been evaluated. + public var launchTime: LaunchTime? + + /// Provides the history of app foreground / background states. + public var applicationStateHistory: AppStateHistory + + // MARK: - Device Specific + + /// Network information. + /// + /// Represents the current state of the device network connectivity and interface. + /// The value can be `unknown` if the network interface is not available or if it has not + /// yet been evaluated. + public var networkConnectionInfo: NetworkConnectionInfo? + + /// Carrier information. + /// + /// Represents the current telephony service info of the device. + /// This value can be `nil` of no service is currently registered, or if the device does + /// not support telephony services. + public var carrierInfo: CarrierInfo? + + /// The current mobile device battery status. + /// + /// This value can be `nil` of the current device battery interface is not available. + public var batteryStatus: BatteryStatus? + + /// `true` if the Low Power Mode is enabled. + public var isLowPowerModeEnabled = false + + /// Type-less context baggages. + public var baggages: [String: FeatureBaggage] = [:] + + // swiftlint:disable function_default_parameter_at_end + public init( + site: DatadogSite, + clientToken: String, + service: String, + env: String, + version: String, + buildNumber: String, + buildId: String?, + variant: String?, + source: String, + sdkVersion: String, + ciAppOrigin: String?, + serverTimeOffset: TimeInterval = .zero, + applicationName: String, + applicationBundleIdentifier: String, + applicationBundleType: BundleType, + sdkInitDate: Date, + device: DeviceInfo, + nativeSourceOverride: String? = nil, + userInfo: UserInfo? = nil, + trackingConsent: TrackingConsent = .pending, + launchTime: LaunchTime? = nil, + applicationStateHistory: AppStateHistory, + networkConnectionInfo: NetworkConnectionInfo? = nil, + carrierInfo: CarrierInfo? = nil, + batteryStatus: BatteryStatus? = nil, + isLowPowerModeEnabled: Bool = false, + baggages: [String: FeatureBaggage] = [:] + ) { + self.site = site + self.clientToken = clientToken + self.service = service + self.env = env + self.version = version + self.buildNumber = buildNumber + self.buildId = buildId + self.variant = variant + self.source = source + self.sdkVersion = sdkVersion + self.ciAppOrigin = ciAppOrigin + self.serverTimeOffset = serverTimeOffset + self.applicationName = applicationName + self.applicationBundleIdentifier = applicationBundleIdentifier + self.applicationBundleType = applicationBundleType + self.sdkInitDate = sdkInitDate + self.device = device + self.nativeSourceOverride = nativeSourceOverride + self.userInfo = userInfo + self.trackingConsent = trackingConsent + self.launchTime = launchTime + self.applicationStateHistory = applicationStateHistory + self.networkConnectionInfo = networkConnectionInfo + self.carrierInfo = carrierInfo + self.batteryStatus = batteryStatus + self.isLowPowerModeEnabled = isLowPowerModeEnabled + self.baggages = baggages + } + // swiftlint:enable function_default_parameter_at_end +} diff --git a/DatadogInternal/Sources/Context/DatadogSite.swift b/DatadogInternal/Sources/Context/DatadogSite.swift new file mode 100644 index 0000000000..636f015d1a --- /dev/null +++ b/DatadogInternal/Sources/Context/DatadogSite.swift @@ -0,0 +1,55 @@ +/* + * 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 + +public enum DatadogSite: String { + /// US based servers. + /// Sends data to [app.datadoghq.com](https://app.datadoghq.com/). + case us1 + /// US based servers. + /// Sends data to [app.datadoghq.com](https://us3.datadoghq.com/). + case us3 + /// US based servers. + /// Sends data to [app.datadoghq.com](https://us5.datadoghq.com/). + case us5 + /// Europe based servers. + /// Sends data to [app.datadoghq.eu](https://app.datadoghq.eu/). + case eu1 + /// Asia based servers. + /// Sends data to [ap1.datadoghq.com](https://ap1.datadoghq.com/). + case ap1 + /// US based servers, FedRAMP compatible. + /// Sends data to [app.ddog-gov.com](https://app.ddog-gov.com/). + case us1_fed + /// US based servers. + /// Sends data to [app.datadoghq.com](https://app.datadoghq.com/). + @available(*, deprecated, message: "Renamed to us1") + public static let us: DatadogSite = .us1 + /// Europe based servers. + /// Sends data to [app.datadoghq.eu](https://app.datadoghq.eu/). + @available(*, deprecated, message: "Renamed to eu1") + public static let eu: DatadogSite = .eu1 + /// Gov servers. + /// Sends data to [app.ddog-gov.com](https://app.ddog-gov.com/). + @available(*, deprecated, message: "Renamed to us1_fed") + public static let gov: DatadogSite = .us1_fed +} + +extension DatadogSite { + public var endpoint: URL { + switch self { + // swiftlint:disable force_unwrapping + case .us1: return URL(string: "https://browser-intake-datadoghq.com/")! + case .us3: return URL(string: "https://browser-intake-us3-datadoghq.com/")! + case .us5: return URL(string: "https://browser-intake-us5-datadoghq.com/")! + case .eu1: return URL(string: "https://browser-intake-datadoghq.eu/")! + case .ap1: return URL(string: "https://browser-intake-ap1-datadoghq.com/")! + case .us1_fed: return URL(string: "https://browser-intake-ddog-gov.com/")! + // swiftlint:enable force_unwrapping + } + } +} diff --git a/DatadogInternal/Sources/Context/DateProvider.swift b/DatadogInternal/Sources/Context/DateProvider.swift new file mode 100644 index 0000000000..ac17720e37 --- /dev/null +++ b/DatadogInternal/Sources/Context/DateProvider.swift @@ -0,0 +1,23 @@ +/* + * 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 + +/// Provides current device time information. +public protocol DateProvider: Sendable { + /// Current device time. + /// + /// A specific point in time, independent of any calendar or time zone. + var now: Date { get } +} + +/// Provides system date. +public struct SystemDateProvider: DateProvider { + public init() { } + + /// Returns current system time. + public var now: Date { .init() } +} diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift new file mode 100644 index 0000000000..9dd865b880 --- /dev/null +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -0,0 +1,232 @@ +/* + * 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 + +/// 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' + public let brand: String + + /// Device marketing name, e.g. "iPhone", "iPad", "iPod touch". + public let name: String + + /// 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, + 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) + } + } +} + +import MachO + +#if canImport(UIKit) +import UIKit + +extension DeviceInfo { + /// Creates device info based on device description. + /// + /// - Parameters: + /// - processInfo: The current process information. + /// - device: The device description. + public init( + 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.osBuild() + let isDebugging = try? sysctl.isDebugging() + let systemBootTime = try? sysctl.systemBootTime() + + #if !targetEnvironment(simulator) + 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, + isSimulator: false, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate + ) + #else + let model = processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? device.model + // 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, + isSimulator: true, + vendorId: device.identifierForVendor?.uuidString, + isDebugging: isDebugging ?? false, + systemBootTime: systemBootTime ?? Date.timeIntervalSinceReferenceDate + ) + #endif + } +} +#elseif os(macOS) +/// Creates device info based on Host description. +/// +/// - Parameters: +/// - processInfo: The current process information. +extension DeviceInfo { + public init( + processInfo: ProcessInfo = .processInfo, + sysctl: SysctlProviding = Sysctl() + ) { + var architecture = "unknown" + if let archInfo = NXGetLocalArchInfo()?.pointee { + architecture = String(utf8String: archInfo.name) ?? "unknown" + } + + 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(), + model: model, + osName: "macOS", + osVersion: "\(systemVersion.majorVersion).\(systemVersion.minorVersion).\(systemVersion.patchVersion)", + osBuildNumber: build, + 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/LaunchTime.swift b/DatadogInternal/Sources/Context/LaunchTime.swift new file mode 100644 index 0000000000..069441fd73 --- /dev/null +++ b/DatadogInternal/Sources/Context/LaunchTime.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 Foundation + +/// Provides the application launch time. +public struct LaunchTime: Codable, Equatable, PassthroughAnyCodable { + /// The app process launch duration (in seconds) measured as the time from process start time + /// to receiving `UIApplication.didBecomeActiveNotification` notification. + /// + /// If the `UIApplication.didBecomeActiveNotification` has not yet been received the value will be `nil`. + public let launchTime: TimeInterval? + + /// The date when the application process started. + public let launchDate: Date + + /// Returns `true` if the application is pre-warmed. + public let isActivePrewarm: Bool + + public init(launchTime: TimeInterval?, launchDate: Date, isActivePrewarm: Bool) { + self.launchTime = launchTime + self.launchDate = launchDate + self.isActivePrewarm = isActivePrewarm + } +} diff --git a/DatadogInternal/Sources/Context/NetworkConnectionInfo.swift b/DatadogInternal/Sources/Context/NetworkConnectionInfo.swift new file mode 100644 index 0000000000..1f2bb55b91 --- /dev/null +++ b/DatadogInternal/Sources/Context/NetworkConnectionInfo.swift @@ -0,0 +1,72 @@ +/* + * 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 + +/// Network connection details. +public struct NetworkConnectionInfo: Codable, Equatable, PassthroughAnyCodable { + /// Tells if network is reachable. + public enum Reachability: String, Codable, CaseIterable { + /// The network is reachable. + case yes + /// The network might be reachable after trying. + case maybe + /// The network is not reachable. + case no + } + + /// Network connection interfaces. + public enum Interface: String, Codable, CaseIterable { + case wifi + case wiredEthernet + case cellular + case loopback + case other + } + + /// Network reachability status. + public let reachability: Reachability + /// Available network interfaces. + public let availableInterfaces: [Interface]? + /// A Boolean indicating whether the connection supports IPv4 traffic. + public let supportsIPv4: Bool? + /// A Boolean indicating whether the connection supports IPv6 traffic. + public let supportsIPv6: Bool? + /// A Boolean indicating if the connection uses an interface that is considered expensive, such as Cellular or a Personal Hotspot. + public let isExpensive: Bool? + /// A Boolean indicating if the connection uses an interface in Low Data Mode. + public let isConstrained: Bool? + + public init( + reachability: Reachability, + availableInterfaces: [Interface]?, + supportsIPv4: Bool?, + supportsIPv6: Bool?, + isExpensive: Bool?, + isConstrained: Bool? + ) { + self.reachability = reachability + self.availableInterfaces = availableInterfaces + self.supportsIPv4 = supportsIPv4 + self.supportsIPv6 = supportsIPv6 + self.isExpensive = isExpensive + self.isConstrained = isConstrained + } +} + +extension NetworkConnectionInfo { + /// Returns an unknown network info with `.maybe` reachability. + static var unknown: NetworkConnectionInfo { + .init( + reachability: .maybe, + availableInterfaces: nil, + supportsIPv4: nil, + supportsIPv6: nil, + isExpensive: nil, + isConstrained: nil + ) + } +} diff --git a/DatadogInternal/Sources/Context/Sysctl.swift b/DatadogInternal/Sources/Context/Sysctl.swift new file mode 100644 index 0000000000..2ab0b7d3f7 --- /dev/null +++ b/DatadogInternal/Sources/Context/Sysctl.swift @@ -0,0 +1,124 @@ +/* + * 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. + * + * This file includes software created by Matt Gallagher on 2016/02/03 and modified by Datadog. + * Copyright © 2016 Matt Gallagher ( https://www.cocoawithlove.com ). + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * Use of this source code is governed by ISC license: https://github.com/mattgallagher/CwlUtils/blob/master/LICENSE.txt + */ + +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 +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 + // Preflight the request to get the required data size + var requiredSize = 0 + let preFlightResult = Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), nil, &requiredSize, nil, 0) + if preFlightResult != 0 { + throw POSIXErrorCode(rawValue: errno).map { + print($0.rawValue) + return Error.posixError($0) + } ?? Error.unknown + } + + // Run the actual request with an appropriately sized array buffer + let data: [Int8] = Array(repeating: 0, count: requiredSize) + let result = data.withUnsafeBufferPointer { dataBuffer -> Int32 in + return Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0) + } + if result != 0 { + throw POSIXErrorCode(rawValue: errno).map { Error.posixError($0) } ?? Error.unknown + } + + return data + } + } + + /// Invoke `sysctl` with an array of identifers, interpreting the returned buffer as a `String`. This function will throw `Error.malformedUTF8` if the buffer returned from `sysctl` cannot be interpreted as a UTF8 buffer. + private static func string(for keys: [Int32]) throws -> String { + let optionalString = try data(for: keys).withUnsafeBufferPointer { dataPointer -> String? in + dataPointer.baseAddress.flatMap { String(validatingUTF8: $0) } + } + guard let s = optionalString else { + throw Error.malformedUTF8 + } + return s + } + + /// e.g. "MacPro4,1" or "iPhone8,1" + /// NOTE: this is *corrected* on iOS devices to fetch hw.machine + 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 + return try Sysctl.string(for: [CTL_HW, HW_MODEL]) + #endif + } + + /// Returns the operating system build as a human-readable string. + /// e.g. "15D21" or "13D20" + 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/Context/TrackingConsent.swift b/DatadogInternal/Sources/Context/TrackingConsent.swift new file mode 100644 index 0000000000..a9c5aff8ad --- /dev/null +++ b/DatadogInternal/Sources/Context/TrackingConsent.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 + +/// Possible values for the Data Tracking Consent given by the user of the app. +/// +/// This value should be used to grant the permission for Datadog SDK to store data collected in +/// Logging, Tracing or RUM and upload it to Datadog servers. +public enum TrackingConsent: Codable, PassthroughAnyCodable { + /// The permission to persist and send data to the Datadog servers was granted. + /// Any previously stored pending data will be marked as ready for sent. + case granted + /// Any previously stored pending data will be deleted and all Logging, RUM and Tracing events will + /// be dropped from now on, without persisting it in any way. + case notGranted + /// All Logging, RUM and Tracing events will be persisted in an intermediate location and will be pending there + /// until `.granted` or `.notGranted` consent value is set. + /// Based on the next consent value, intermediate data will be send to Datadog or deleted. + case pending +} diff --git a/DatadogInternal/Sources/Context/UserInfo.swift b/DatadogInternal/Sources/Context/UserInfo.swift new file mode 100644 index 0000000000..83ee22de84 --- /dev/null +++ b/DatadogInternal/Sources/Context/UserInfo.swift @@ -0,0 +1,71 @@ +/* + * 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 + +public struct UserInfo: Codable, PassthroughAnyCodable { + /// User ID, if any. + public let id: String? + /// Name representing the user, if any. + public let name: String? + /// User email, if any. + public let email: String? + /// User custom attributes, if any. + public var extraInfo: [AttributeKey: AttributeValue] + + enum CodingKeys: String, CodingKey { + case id + case name + case email + } + + public init( + id: String? = nil, + name: String? = nil, + email: String? = nil, + extraInfo: [AttributeKey: AttributeValue] = [:] + ) { + self.id = id + self.name = name + self.email = email + self.extraInfo = extraInfo + } + + public func encode(to encoder: Encoder) throws { + // Encode static properties: + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(id, forKey: .id) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(email, forKey: .email) + + // Encode dynamic properties: + var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) + try extraInfo.forEach { + let key = DynamicCodingKey($0) + try dynamicContainer.encode(AnyEncodable($1), forKey: key) + } + } + + public init(from decoder: Decoder) throws { + // Decode static properties: + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: .id) + self.name = try container.decodeIfPresent(String.self, forKey: .name) + self.email = try container.decodeIfPresent(String.self, forKey: .email) + + // Decode other properties into [String: Codable] dictionary: + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + self.extraInfo = try dynamicContainer.allKeys + .filter { CodingKeys(stringValue: $0.stringValue) == nil } + .reduce(into: [:]) { + $0[$1.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: $1) + } + } +} + +extension UserInfo { + public static var empty: Self { .init() } +} diff --git a/DatadogInternal/Sources/CoreRegistry.swift b/DatadogInternal/Sources/CoreRegistry.swift new file mode 100644 index 0000000000..54d0b771e6 --- /dev/null +++ b/DatadogInternal/Sources/CoreRegistry.swift @@ -0,0 +1,92 @@ +/* + * 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 Registry for all core instances, allowing Features to retrieve the one +/// they want from anywhere. +public final class CoreRegistry { + /// Returns the default core instance if registered, `NOPDatadogCore` instance otherwise. + public static var `default`: DatadogCoreProtocol { + instances[defaultInstanceName] ?? NOPDatadogCore() + } + + /// The name for the default core instance. + /// + /// Features should use this name as default parameter. + public static let defaultInstanceName = "main" + + @ReadWriteLock + internal private(set) static var instances: [String: DatadogCoreProtocol] = [:] + + private init() { } + + /// Register default core instance. + /// + /// - Parameter instance: The default core instance + public static func register(default instance: DatadogCoreProtocol) { + register(instance, named: defaultInstanceName) + } + + /// Register an instance of core instance with the given name. + /// + /// - Parameters: + /// - instance: The core instance + /// - name: The name of the given instance. + public static func register(_ instance: DatadogCoreProtocol, named name: String) { + guard !isRegistered(instanceName: name) else { + DD.logger.warn("A core instance with name \(name) has already been registered.") + return + } + instances[name] = instance + } + + /// Checks if a core instance with the specified name is currently registered. + /// + /// - Parameter instanceName: The name of the core instance to check. + /// - Returns: `true` if an instance with the given name is registered, otherwise `false`. + public static func isRegistered(instanceName: String) -> Bool { + return instances[instanceName] != nil + } + + /// Unregisters the instance for the given name. + /// + /// - Parameter name: The name of the instance to unregister. + /// - Returns: The instance that was removed, or nil if the key was not present in the registry. + @discardableResult + public static func unregisterInstance(named name: String) -> DatadogCoreProtocol? { + instances.removeValue(forKey: name) + } + + /// Unregisters the default instance. + /// + /// - Returns: The instance that was removed, or nil if the key was not present in the registry. + @discardableResult + public static func unregisterDefault() -> DatadogCoreProtocol? { + unregisterInstance(named: defaultInstanceName) + } + + /// Returns the instance for the given name. + /// + /// - Parameter name: The name of the instance to get. + /// - Returns: The core instance if it exists, `NOPDatadogCore` instance otherwise. + 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 new file mode 100644 index 0000000000..67262e3f4f --- /dev/null +++ b/DatadogInternal/Sources/DD.swift @@ -0,0 +1,55 @@ +/* + * 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 + +/// Core utilities for monitoring performance and execution of the SDK. +/// +/// These are meant to be shared by all instances of the SDK and `DatadogCore`. +/// `DD` bundles static dependencies that must be available and functional right away, +/// so it is possible to monitor any phase of the SDK execution, including its initialization sequence. +public struct DD { + /// The logger providing methods to print debug information and execution errors from Datadog SDK to user console. + /// + /// It is meant for debugging purposes when using the SDK, hence **it should log information useful and actionable + /// to the SDK user**. Think of possible logs that we may want to receive from our users when asking them to enable + /// SDK verbosity and send us their console log. + public static var logger: CoreLogger = InternalLogger( + dateProvider: SystemDateProvider(), + timeZone: .current, + printFunction: consolePrint, + verbosityLevel: { .debug } + ) +} + +#if canImport(OSLog) +import OSLog +#endif + +/// Function printing `String` content to console. +public var consolePrint: @Sendable (String, CoreLoggerLevel) -> Void = { message, level in + #if canImport(OSLog) + if #available(iOS 14.0, tvOS 14.0, *) { + switch level { + case .debug: Logger.datadog.debug("\(message, privacy: .private)") + case .warn: Logger.datadog.warning("\(message, privacy: .private)") + case .error: Logger.datadog.critical("\(message, privacy: .private)") + case .critical: Logger.datadog.fault("\(message, privacy: .private)") + } + } else { + print(message) + } + #else + print(message) + #endif +} + +#if canImport(OSLog) +@available(iOS 14.0, tvOS 14.0, *) +extension Logger { + static let datadog = Logger(subsystem: "dd-sdk-ios", category: "DatadogInternal") +} +#endif diff --git a/DatadogInternal/Sources/DataStore/DataStore.swift b/DatadogInternal/Sources/DataStore/DataStore.swift new file mode 100644 index 0000000000..e437ac639b --- /dev/null +++ b/DatadogInternal/Sources/DataStore/DataStore.swift @@ -0,0 +1,94 @@ +/* + * 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 + +/// Represents the version of data format stored for a key in the data store. +/// +/// As values are persisted, the format of data may change between different SDK versions. To prevent decoding errors, +/// callers can utilize `DataStoreKeyVersion` to explicitly specify the version of serialized `Data` before attempting deserialization. +public typealias DataStoreKeyVersion = UInt16 + +/// The default version of data stored for keys (equals `0`). +public let dataStoreDefaultKeyVersion: DataStoreKeyVersion = 0 + +/// Possible results of retrieving a value from the data store. +public enum DataStoreValueResult { + /// The value was found and serialized using the format defined by the specified version. + case value(Data, DataStoreKeyVersion) + /// There was no value associated with the requested key. + case noValue + /// The value could not be read due to an underlying error. + /// This may represent an error with the file format or an I/O exception that occurred during reading. + case error(Error) + + /// Retrieves the data value associated with the result, if it matches the expected version. + /// + /// - Parameter expectedVersion: The version expected for the retrieved data. + /// - Returns: The data value if the version matches the expected version; otherwise, nil. + public func data(expectedVersion: DataStoreKeyVersion = dataStoreDefaultKeyVersion) -> Data? { + guard case .value(let data, let storedVersion) = self, storedVersion == expectedVersion else { + return nil + } + return data + } +} + +/// Defines the interface for a data store capable of storing key-value pairs for a given feature. +public protocol DataStore { + /// Sets the value for the specified key in the data store. + /// + /// - Parameters: + /// - value: The data to be stored. + /// - key: The unique identifier for the data. Must be a valid file name, as it will be persisted in files. + /// - version: The version of the data format. Defaults to `0`. + func setValue(_ value: Data, forKey key: String, version: DataStoreKeyVersion) + + /// Retrieves the value associated with the specified key from the data store. + /// + /// - Parameters: + /// - key: The unique identifier for the data. Must be a valid file name, as it will be persisted in files. + /// - callback: A closure providing the result asynchronously on an internal queue. + /// + /// Note: The implementation must log errors to console and notify them through telemetry. Callers are not required + /// to implement logging of errors upon receiving `.error()` result. + func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) + + /// Deletes the value associated with the specified key from the data store. + /// + /// - 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 { + /// Sets the value for the specified key in the data store with the default `version` of `0`. + /// + /// - Parameters: + /// - value: The data to be stored. + /// - key: The unique identifier for the data. Must be a valid file name, as it will be persisted in files. + func setValue(_ value: Data, forKey key: String) { + setValue(value, forKey: key, version: dataStoreDefaultKeyVersion) + } +} + +public struct NOPDataStore: DataStore { + public init() {} + + /// no-op + public func setValue(_ value: Data, forKey key: String, version: DataStoreKeyVersion) {} + /// no-op + 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 new file mode 100644 index 0000000000..486ad1ab97 --- /dev/null +++ b/DatadogInternal/Sources/DatadogCoreProtocol.swift @@ -0,0 +1,328 @@ +/* + * 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 Core holds a set of Features and is responsible for managing their storage +/// and upload mechanism. It also provides a thread-safe scope for writing events. +/// +/// 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, Storage { + // Remove `DatadogCoreProtocol` conformance to `MessageSending` and `BaggageSharing` once + // all features are migrated to depend on `FeatureScope` interface. + + /// Registers a Feature instance. + /// + /// Feature can interact with the core and other Feature through the message bus. Some specific Features + /// complying to `DatadogRemoteFeature` can collect and transfer data to a Datadog Product + /// (e.g. Logs, RUM, ...). Upon registration, a Remote Feature can retrieve a `FeatureScope` interface + /// for writing events to the core. The core will store and upload events efficiently according to the performance + /// presets defined on initialization. + /// + /// - Parameter feature: The Feature instance - it will be retained and held by core. + func register(feature: T) throws where T: DatadogFeature + + /// Retrieves previously registered Feature by its name and type. + /// + /// A Feature type can be specified as parameter or inferred from the return type: + /// + /// let feature = core.feature(named: "foo", type: Foo.self) + /// let feature: Foo? = core.feature(named: "foo") + /// + /// - Parameters: + /// - name: The Feature's name. + /// - type: The Feature instance type. + /// - Returns: The Feature if any. + func feature(named name: String, type: T.Type) -> T? + + /// Retrieves a Feature Scope for given feature type. + /// + /// The scope manages the underlying core's reference in safe way, guaranteeing no reference leaks. + /// It is available right away even before the feature registration completes in the core, so some capabilities + /// might be not available before the feature is fully registered. + /// + /// If possible, feature implementation must to take dependency on `FeatureScope` rather than `DatadogCoreProtocol` itself. + /// + /// - Parameters: + /// - type: The Feature instance type. + /// - Returns: The scope for requested feature type. + func scope(for featureType: T.Type) -> FeatureScope where T: DatadogFeature +} + +public protocol MessageSending { + /// Sends a message on the bus shared by features registered to the sam core. + /// + /// If the message could not be processed by any registered feature, the fallback closure + /// will be invoked. Do not make any assumption on which thread the fallback is called. + /// + /// - Parameters: + /// - message: The message. + /// - fallback: The fallback closure to call when the message could not be + /// processed by any Features on the bus. + func send(message: FeatureMessage, else fallback: @escaping () -> Void) +} + +public protocol BaggageSharing { + /// Sets given baggage for a given Feature for sharing data through `DatadogContext`. + /// + /// This method provides a passive communication chanel between Features of the Core. + /// For an active Feature-to-Feature communication, please use the `send(message:)` + /// method. + /// + /// Setting baggages will update the Core Context that is shared across Features. + /// In the following examples, the Feature `foo` will set an value and a second + /// Feature `bar` will read it through the event write context. + /// + /// // Foo.swift + /// core.set(baggage: { .init("value") }, forKey: "key") + /// + /// // Bar.swift + /// core.scope(for: "bar").eventWriteContext { context, writer in + /// if let baggage = context.baggages["key"] { + /// do { + /// // Try decoding context to expected type: + /// let value: String = try baggage.decode() + /// // If success, handle the `value`. + /// } catch { + /// // Otherwise, handle the error (e.g. consider sending as telemetry). + /// } + /// } + /// } + /// + /// - Parameters: + /// - baggage: The Feature's baggage to set. + /// - key: The baggage's key. + func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) +} + +extension DatadogCoreProtocol { + /// Returns a `DatadogFeature` conforming type from the + /// Feature registry. + /// + /// - Parameter type: The Feature instance type. + /// - Returns: The Feature if any. + public func get(feature type: T.Type = T.self) -> T? where T: DatadogFeature { + feature(named: T.name, type: type) + } +} + +extension MessageSending { + /// Sends a message on the bus shared by features registered to the same core. + /// + /// - Parameters: + /// - message: The message. + public func send(message: FeatureMessage) { + send(message: message, else: {}) + } +} + +extension BaggageSharing { + /// Sets given baggage for a given Feature for sharing data through `DatadogContext`. + /// + /// This method provides a passive communication chanel between Features of the Core. + /// For an active Feature-to-Feature communication, please use the `send(message:)` + /// method. + /// + /// Setting baggages will update the Core Context that is shared across Features. + /// In the following examples, the Feature `foo` will set an value and a second + /// Feature `bar` will read it through the event write context. + /// + /// // Foo.swift + /// core.set(baggage: FeatureBaggage("value"), forKey: "key") + /// + /// // Bar.swift + /// core.scope(for: "bar").eventWriteContext { context, writer in + /// if let baggage = context.baggages["key"] { + /// try { + /// // Try decoding context to expected type: + /// let value: String = try baggage.decode() + /// // If success, handle the `value`. + /// } catch { + /// // Otherwise, handle the error (e.g. consider sending as telemetry). + /// } + /// } + /// } + /// + /// - Parameters: + /// - baggage: The Feature's baggage to set. + /// - label: The baggage's label. + public func set(baggage: FeatureBaggage?, forKey key: String) { + self.set(baggage: { baggage }, forKey: key) + } + + /// Sets given baggage for a given Feature for sharing data through `DatadogContext`. + /// + /// This method provides a passive communication chanel between Features of the Core. + /// For an active Feature-to-Feature communication, please use the `send(message:)` + /// method. + /// + /// Setting baggages will update the Core Context that is shared across Features. + /// In the following examples, the Feature `foo` will set an value and a second + /// Feature `bar` will read it through the event write context. + /// + /// // Foo.swift + /// core.set(baggage: "value", forKey: "key") + /// + /// // Bar.swift + /// core.scope(for: "bar").eventWriteContext { context, writer in + /// if let baggage = context.baggages["key"] { + /// try { + /// // Try decoding context to expected type: + /// let value: String = try baggage.decode() + /// // If success, handle the `value`. + /// } catch { + /// // Otherwise, handle the error (e.g. consider sending as telemetry). + /// } + /// } + /// } + /// + /// - Parameters: + /// - baggage: The Feature's baggage to set. + /// - label: The baggage's label. + public func set(baggage: Baggage?, forKey key: String) where Baggage: Encodable { + self.set(baggage: { baggage }, forKey: key) + } + + /// Sets given baggage for a given Feature for sharing data through `DatadogContext`. + /// + /// This method provides a passive communication chanel between Features of the Core. + /// For an active Feature-to-Feature communication, please use the `send(message:)` + /// method. + /// + /// Setting baggages will update the Core Context that is shared across Features. + /// In the following examples, the Feature `foo` will set an value and a second + /// Feature `bar` will read it through the event write context. + /// + /// // Foo.swift + /// core.set(baggage: { "value" }, forKey: "key") + /// + /// // Bar.swift + /// core.scope(for: "bar").eventWriteContext { context, writer in + /// if let baggage = context.baggages["key"] { + /// try { + /// // Try decoding context to expected type: + /// let value: String = try baggage.decode() + /// // If success, handle the `value`. + /// } catch { + /// // Otherwise, handle the error (e.g. consider sending as telemetry). + /// } + /// } + /// } + /// + /// - Parameters: + /// - baggage: The Feature's baggage to set. + /// - label: The baggage's label. + public func set(baggage: @escaping () -> Baggage?, forKey key: String) where Baggage: Encodable { + self.set(baggage: { baggage().map(FeatureBaggage.init) }, forKey: key) + } +} + +/// Feature scope provides a context and a writer to build a record event. +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. + /// The provided context is valid at the moment of the call, meaning that it includes all changes that happened + /// earlier on the same thread. + /// + /// A Feature has the ability to bypass the current user consent for data collection. Set `bypassConsent` to `true` + /// only if the Feature is already aware of the user's consent for the event it is about to write. + /// + /// - Parameters: + /// - bypassConsent: `true` to bypass the current core consent and write events as authorized. + /// Default is `false`, setting `true` must still respect user's consent for + /// collecting information. + /// - block: The block to execute; it is called on the context queue. + func eventWriteContext(bypassConsent: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) + + /// Retrieve the core context. + /// + /// A feature can use this method to request the Datadog context valid at the moment of the call. + /// + /// - Parameter block: The block to execute; it is called on the context queue. + func context(_ block: @escaping (DatadogContext) -> Void) + + /// Data store endpoint. + /// + /// Use this property to store data for this feature. Data will be persisted between app launches. + var dataStore: DataStore { get } + + /// Telemetry endpoint. + /// + /// Use this property to report any telemetry event to the core. + var telemetry: Telemetry { get } +} + +/// Feature scope provides a context and a writer to build a record event. +public extension FeatureScope { + /// Retrieve the core context and event writer. + /// + /// The Feature scope provides the current Datadog context and event writer for building and recording events. + /// The provided context is valid at the moment of the call, meaning that it includes all changes that happened + /// earlier on the same thread. + /// + /// A Feature has the ability to bypass the current user consent for data collection. Set `bypassConsent` to `true` + /// only if the Feature is already aware of the user's consent for the event it is about to write. + /// + /// - Parameters: + /// - bypassConsent: `true` to bypass the current core consent and write events as authorized. + /// Default is `false`, setting `true` must still respect user's consent for + /// collecting information. + /// - forceNewBatch: `true` to enforce that event will be written to a separate batch than previous events. + /// Default is `false`, which means the core uses its own heuristic to split events between + /// batches. This parameter can be leveraged in Features which require a clear separation + /// of group of events for preparing their upload (a single upload is always constructed from a single batch). + /// - block: The block to execute; it is called on the context queue. + func eventWriteContext(_ block: @escaping (DatadogContext, Writer) -> Void) { + eventWriteContext(bypassConsent: false, block) + } + + /// Retrieve the core context and data store. + /// + /// Can be used to store data that depends on the current Datadog context. The provided context is valid at the moment + /// of the call, meaning that it includes all changes that happened earlier on the same thread. + /// + /// - Parameter block: The block to execute; it is called on the context queue. + func dataStoreContext(_ block: @escaping (DatadogContext, DataStore) -> Void) { + context { context in + block(context, dataStore) + } + } +} + +/// No-op implementation of `DatadogFeatureRegistry`. +public class NOPDatadogCore: DatadogCoreProtocol { + public init() { } + /// no-op + public func register(feature: T) throws where T: DatadogFeature { } + /// no-op + public func feature(named name: String, type: T.Type) -> T? { nil } + /// no-op + public func scope(for featureType: T.Type) -> FeatureScope { NOPFeatureScope() } + /// no-op + 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 { + public init() { } + /// no-op + public func eventWriteContext(bypassConsent: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) { } + /// no-op + public func context(_ block: @escaping (DatadogContext) -> Void) { } + /// no-op + public var dataStore: DataStore { NOPDataStore() } + /// no-op + public var telemetry: Telemetry { NOPTelemetry() } + /// no-op + public func send(message: FeatureMessage, else fallback: @escaping () -> Void) { } + /// no-op + public func set(baggage: @escaping () -> FeatureBaggage?, forKey key: String) { } +} diff --git a/DatadogInternal/Sources/DatadogFeature.swift b/DatadogInternal/Sources/DatadogFeature.swift new file mode 100644 index 0000000000..6a726d0532 --- /dev/null +++ b/DatadogInternal/Sources/DatadogFeature.swift @@ -0,0 +1,38 @@ +/* + * 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 Feature that can interact with the core through the message-bus. +public protocol DatadogFeature { + /// The feature name. + static var name: String { get } + + /// The message bus receiver. + /// + /// The `FeatureMessageReceiver` defines an interface for Feature to receive any message + /// from a bus that is shared between Features registered in a core. + var messageReceiver: FeatureMessageReceiver { get } + + /// (Optional) `PerformancePresetOverride` allows overriding certain performance presets if needed. + var performanceOverride: PerformancePresetOverride? { get } +} + +/// A Datadog Feature with remote data store. +public protocol DatadogRemoteFeature: DatadogFeature { + /// The URL request builder for uploading data. + /// + /// The `FeatureRequestBuilder` defines an interface for building a single `URLRequest` + /// for a list of data events and the current core context. + /// + /// A Feature should use this interface for creating requests that needs be sent to its Datadog Intake. + /// The request will be transported by `DatadogCore`. + var requestBuilder: FeatureRequestBuilder { get } +} + +extension DatadogFeature { + public var performanceOverride: PerformancePresetOverride? { nil } +} 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 new file mode 100644 index 0000000000..7d37267226 --- /dev/null +++ b/DatadogInternal/Sources/Extensions/DatadogExtended.swift @@ -0,0 +1,51 @@ +/* + * 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. + * + * This file includes software developed by Alamofire Software Foundation (http://alamofire.org/) and altered by Datadog. + * Use of this source code is governed by MIT License: https://github.com/Alamofire/Alamofire/blob/master/LICENSE + */ + +import Foundation + +/// Type that acts as a generic extension point for all `DatadogExtended` types. +public struct DatadogExtension { + /// Stores the type or meta-type of any extended type. + public private(set) var type: ExtendedType + + /// Create an instance from the provided value. + /// + /// - Parameter type: Instance being extended. + public init(_ type: ExtendedType) { + self.type = type + } +} + +/// Protocol describing the `dd` extension points for Datadog extended types. +public protocol DatadogExtended { + /// Type being extended. + associatedtype ExtendedType + + /// Static Datadog extension point. + static var dd: DatadogExtension.Type { get set } + /// Instance Datadog extension point. + var dd: DatadogExtension { get set } +} + +extension DatadogExtended { + /// Static Datadog extension point. + public static var dd: DatadogExtension.Type { + get { DatadogExtension.self } + set {} + } + + /// Instance Datadog extension point. + public var dd: DatadogExtension { + get { DatadogExtension(self) } + set {} + } +} + +extension Array: DatadogExtended {} +extension Dictionary: DatadogExtended {} diff --git a/DatadogInternal/Sources/Extensions/FixedWidthInteger+Convenience.swift b/DatadogInternal/Sources/Extensions/FixedWidthInteger+Convenience.swift new file mode 100644 index 0000000000..07f276be82 --- /dev/null +++ b/DatadogInternal/Sources/Extensions/FixedWidthInteger+Convenience.swift @@ -0,0 +1,32 @@ +/* + * 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 + +/// An extension for FixedWidthInteger that provides a convenient API for +/// converting numeric values into different units of data storage, such as +/// bytes, kilobytes, megabytes, and gigabytes. +public extension FixedWidthInteger { + /// A private property that represents the base unit (1024) used for + /// converting between data storage units. + private var base: Self { 1_024 } + + /// A property that converts the given numeric value into kilobytes. + var KB: Self { return self.multipliedReportingOverflow(by: base).partialValue } + + /// A property that converts the given numeric value into megabytes. + var MB: Self { return self.KB.multipliedReportingOverflow(by: base).partialValue } + + /// A property that converts the given numeric value into gigabytes. + var GB: Self { return self.MB.multipliedReportingOverflow(by: base).partialValue } + + /// A helper property that returns the current value as a direct representation in bytes. + var bytes: Self { return self } + + func asUInt64() -> UInt64 { + return UInt64(self) + } +} diff --git a/DatadogInternal/Sources/Extensions/Foundation+Datadog.swift b/DatadogInternal/Sources/Extensions/Foundation+Datadog.swift new file mode 100644 index 0000000000..47bcde053d --- /dev/null +++ b/DatadogInternal/Sources/Extensions/Foundation+Datadog.swift @@ -0,0 +1,19 @@ +/* + * 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 + +extension Thread: DatadogExtended {} +extension DatadogExtension where ExtendedType: Thread { + /// Returns the name of current thread if available or the nature of thread otherwise: `"main" | "background"`. + public var name: String { + if let name = Thread.current.name, !name.isEmpty { + return name + } + + return Thread.isMainThread ? "main" : "background" + } +} diff --git a/DatadogInternal/Sources/Extensions/InternalExtended.swift b/DatadogInternal/Sources/Extensions/InternalExtended.swift new file mode 100644 index 0000000000..45ae1e6e77 --- /dev/null +++ b/DatadogInternal/Sources/Extensions/InternalExtended.swift @@ -0,0 +1,55 @@ +/* + * 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 + +/// Type that acts as a generic extension point for all `InternalExtended` types. +public struct InternalExtension { + /// Stores the type or meta-type of any extended type. + public var type: ExtendedType + + /// Create an instance from the provided value. + /// + /// - Parameter type: Instance being extended. + public init(_ type: ExtendedType) { + self.type = type + } +} + +/// Protocol describing the `_internal` extension points for internal extended types. +public protocol InternalExtended { + /// Type being extended. + associatedtype ExtendedType + + /// Static internal extension point. + static var _internal: InternalExtension.Type { get } + + /// Instance internal extension point. + var _internal: InternalExtension { get } + + /// Instance internal mutation point. + mutating func _internal_mutation(_ mutation: (inout InternalExtension) -> Void) +} + +extension InternalExtended { + /// Grants access to an internal interface utilized only by other Datadog SDKs. + /// **It is not meant for public use** and it might change without prior notice. + public static var _internal: InternalExtension.Type { + InternalExtension.self + } + + /// Instance internal extension point. + public var _internal: InternalExtension { + InternalExtension(self) + } + + /// Instance internal mutaion point. + public mutating func _internal_mutation(_ mutation: (inout InternalExtension) -> Void) { + var mutating = InternalExtension(self) + mutation(&mutating) + self = mutating.type + } +} diff --git a/DatadogInternal/Sources/Extensions/TimeInterval+Convenience.swift b/DatadogInternal/Sources/Extensions/TimeInterval+Convenience.swift new file mode 100644 index 0000000000..9dfdb783bb --- /dev/null +++ b/DatadogInternal/Sources/Extensions/TimeInterval+Convenience.swift @@ -0,0 +1,71 @@ +/* + * 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 + +/// An extension for TimeInterval that provides a more semantic and expressive +/// API for converting time representations into TimeInterval's default unit: seconds. +public extension TimeInterval { + /// A helper property that returns the current value as a direct representation in seconds. + var seconds: TimeInterval { return TimeInterval(self) } + + /// A property that converts the given number of minutes into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var minutes: TimeInterval { return self.multiplyOrClamp(by: 60) } + + /// A property that converts the given number of hours into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var hours: TimeInterval { return self.multiplyOrClamp(by: 60.minutes) } + + /// A property that converts the given number of days into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var days: TimeInterval { return self.multiplyOrClamp(by: 24.hours) } + + /// A private helper method for multiplying the TimeInterval value by a factor + /// and clamping the result to prevent overflow. If the multiplication results in + /// overflow, the greatest finite magnitude value of TimeInterval is returned. + /// + /// - Parameter factor: The multiplier to apply to the time interval. + /// - Returns: The multiplied time interval, clamped to the greatest finite magnitude if necessary. + private func multiplyOrClamp(by factor: TimeInterval) -> TimeInterval { + guard factor != 0 else { + return 0 + } + let multiplied = TimeInterval(self) * factor + if multiplied / factor != TimeInterval(self) { + return TimeInterval.greatestFiniteMagnitude + } + return multiplied + } +} + +/// An extension for FixedWidthInteger that provides a more semantic and expressive +/// API for converting time representations into TimeInterval's default unit: seconds. +public extension FixedWidthInteger { + /// A helper property that returns the current value as a direct representation in seconds. + var seconds: TimeInterval { return TimeInterval(self) } + + /// A property that converts the given numeric value of minutes into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var minutes: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: 60) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } + + /// A property that converts the given numeric value of hours into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var hours: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: Self(60.minutes)) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } + + /// A property that converts the given numeric value of days into seconds. + /// In case of overflow, TimeInterval.greatestFiniteMagnitude is returned. + var days: TimeInterval { + let (result, overflow) = self.multipliedReportingOverflow(by: Self(24.hours)) + return overflow ? TimeInterval.greatestFiniteMagnitude : TimeInterval(result) + } +} diff --git a/DatadogInternal/Sources/MessageBus/FeatureBaggage.swift b/DatadogInternal/Sources/MessageBus/FeatureBaggage.swift new file mode 100644 index 0000000000..035e5cafe7 --- /dev/null +++ b/DatadogInternal/Sources/MessageBus/FeatureBaggage.swift @@ -0,0 +1,87 @@ +/* + * 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 `FeatureBaggage` holds any codable value. +/// +/// Values are uniquely identified by key as `String`, the value type is validated on `get` +/// either explicity, when specified, or inferred. +/// +/// ## Creates a Feature Baggage +/// Create a baggage by providing an `Encodable` value. +/// +/// The example below shows how to create a baggage from an instance of a simple `GroceryProduct`: +/// +/// struct GroceryProduct: Encodable { +/// var name: String +/// var points: Int +/// var description: String? +/// } +/// +/// let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.") +/// let baggage = FeatureBaggage(pear) +/// +/// The `pear` is stored as a dictionary within the baggage: +/// +/// print(baggage.rawValue) +/// // ["description": Optional("A ripe pear."), "points": Optional(250), "name": Optional("Pear")] +/// +/// ## Accessing Value +/// The baggage value can then be decoded to any type that follow the same schema. +/// +/// The following example decodes the baggage into a new data type: +/// +/// struct CartItem: Decodable { +/// var name: String +/// var points: Int +/// } +/// +/// let item: CartItem = try baggage.decode() +/// +/// print(item) +/// // CartItem(name: "Pear", points: 250) +/// +/// A Feature Baggage does not ensure thread-safety of values that holds references, make +/// sure that any value can be accessibe from any thread. +public final class FeatureBaggage { + /// The raw value contained in the baggage. + @ReadWriteLock + private var rawValue: Any? + + /// The underlying encoding process. + private let _encode: () throws -> Any? + + /// Creates an instance initialized with the given encodable. + public init(_ value: Value) where Value: Encodable { + let encoder = AnyEncoder() + self._encode = { try encoder.encode(value) } + } + + /// Encodes the baggage value to `Any?` + /// + /// - Returns: The encoded baggage value. + public func encode() throws -> Any? { + // lazily encode to save from encoding + // if the value is never decoded. + if let rawValue = self.rawValue { + return rawValue + } + let rawValue = try _encode() + self.rawValue = rawValue + return rawValue + } + + /// Decodes the value stored in the baggage. + /// + /// - Parameters: + /// - type: The expected value type. + /// - Returns: The decoded baggage. + public func decode(type: Value.Type = Value.self) throws -> Value where Value: Decodable { + let decoder = AnyDecoder() + return try decoder.decode(from: encode()) + } +} diff --git a/DatadogInternal/Sources/MessageBus/FeatureMessage.swift b/DatadogInternal/Sources/MessageBus/FeatureMessage.swift new file mode 100644 index 0000000000..5e462c83a9 --- /dev/null +++ b/DatadogInternal/Sources/MessageBus/FeatureMessage.swift @@ -0,0 +1,69 @@ +/* + * 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 + +/// The set of messages that can be transimtted on the Features message bus. +public enum FeatureMessage { + /// A custom message with generic encodable attributes. + /// + /// A baggage can be used to transmit loosely-typed data structure using `Codable`. + /// The encoding/decoding processes will have an impact on performances, opt for a baggage + /// only if the data-structure is small. + /// + /// For large data type, use the `.value` case with shared type definition. + case baggage( + key: String, + baggage: FeatureBaggage + ) + + /// A web-view message. + /// + /// Represent a Browser SDK event sent through the JS bridge. + case webview(WebViewMessage) + + /// A core context message. + /// + /// The core will send updated context throught the bus. Do not send new context values + /// from a Feature or Integration. + case context(DatadogContext) + + /// A telemtry message. + /// + /// The core can send telemtry data coming from all Features. + case telemetry(TelemetryMessage) +} + +extension FeatureMessage { + /// Creates a `.baggage` message with the given key and `Encodable` value. + /// + /// A baggage can be used to transmit loosely-typed data structure using `Codable`. + /// The encoding/decoding processes will have an impact on performances, opt for a baggage + /// only if the data-structure is small. + /// + /// - Parameters: + /// - key: The baggage key. + /// - baggage: The baggage value. + /// - Returns: a `.baggage` case. + public static func baggage(key: String, value: Value) -> FeatureMessage where Value: Encodable { + .baggage(key: key, baggage: .init(value)) + } + + /// Returns the baggage if the key matches the message. + /// + /// - Parameters: + /// - key: The requested baggage key. + /// - type: The expected type of the baggage value. + /// - Returns: The decoded baggage value, or nil if the key doesn't match. + /// - Throws: A `DecodingError` if decoding fails. + public func baggage(forKey key: String, type: Value.Type = Value.self) throws -> Value? where Value: Decodable { + guard case let .baggage(messageKey, baggage) = self, messageKey == key else { + return nil + } + + return try baggage.decode(type: type) + } +} diff --git a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift new file mode 100644 index 0000000000..89558283bd --- /dev/null +++ b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift @@ -0,0 +1,69 @@ +/* + * 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 + +/// The `FeatureMessageReceiver` defines an interface for a Feature to receive messages +/// from a bus that is shared between Features registered to same instance of the core. +/// +/// The message is composed of a key and a dictionary of attributes. The message format is a loose +/// agreement between Features - all messages supported by a Feature should be properly documented. +public protocol FeatureMessageReceiver { + /// Receives messages from the message bus. + /// + /// The message can be used to build an event or execute custom routine in the Feature. + /// + /// This method is always called on the same thread managed by core. If the implementation + /// of `FeatureMessageReceiver` needs to manage a state it can consider its mutations started + /// from `receive(message:from:)` to be thread-safe. The implementation should be mindful of + /// not blocking the caller thread to not delay processing of other messages in the system. + /// + /// - Parameters: + /// - message: The message. + /// - core: An instance of the core from which the message is transmitted. + /// - Returns: `true` if the message was processed by the receiver;`false` if it was ignored. + @discardableResult + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool + // ^ TODO: RUM-3717 + // Remove `core:` parameter from this API once all features are migrated to depend on `FeatureScope` interface + // instead of depending on directly on `core`. +} + +public struct NOPFeatureMessageReceiver: FeatureMessageReceiver { + public init() { } + + /// no-op: returns `false` + public func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + return false + } +} + +/// 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] + + /// Creates an instance initialized with the given receivers. + public init(_ receivers: FeatureMessageReceiver...) { + self.receivers = Array(receivers) + } + + /// Creates an instance initialized with the given receivers. + public init(_ receivers: [FeatureMessageReceiver]) { + self.receivers = receivers + } + + /// Receiving a message will loop though receivers and stop on the first that is able to + /// consume the given message. + /// + /// - Parameters: + /// - message: The message. + /// - core: An instance of the core from which the message is transmitted. + /// - Returns: `true` if the message was processed by one of the receiver; `false` if it was ignored. + public func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + receivers.contains(where: { $0.receive(message: message, from: core) }) + } +} diff --git a/DatadogInternal/Sources/Models/CrashReporting/BacktraceReport.swift b/DatadogInternal/Sources/Models/CrashReporting/BacktraceReport.swift new file mode 100644 index 0000000000..9c8fede2c7 --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/BacktraceReport.swift @@ -0,0 +1,41 @@ +/* + * 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 snapshot of all running threads in the current process. It focuses on tracing back from the error point (where backtrace +/// generation started) to the root cause or the origin of the problem. +/// +/// - Unlike `DDCrashReport`, the backtrace report can be generated on-demand without the actual crash being triggered. +/// - Like in `DDCrashReport`, threads and stacks information in `BacktraceReport` follows the format compatible with Datadog symbolication. +public struct BacktraceReport: Codable { + /// The stack trace of the thread for which the backtrace is generated. + public let stack: String + /// Represents all threads currently running in the process. + public let threads: [DDThread] + /// A list of binary images referenced from all stack traces. + public let binaryImages: [BinaryImage] + /// Indicates whether any stack trace information in `threads` was truncated due to stack trace minimization. + public let wasTruncated: Bool + + /// Initializes a new instance of `BacktraceReport`. + /// - Parameters: + /// - stack: The stack trace of the thread. + /// - threads: All threads currently running in the process. + /// - binaryImages: A list of binary images referenced from all stack traces. + /// - wasTruncated: Indicates whether stack trace information was truncated. + public init( + stack: String, + threads: [DDThread], + binaryImages: [BinaryImage], + wasTruncated: Bool + ) { + self.stack = stack + self.threads = threads + self.binaryImages = binaryImages + self.wasTruncated = wasTruncated + } +} diff --git a/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift new file mode 100644 index 0000000000..7d53e3be5d --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift @@ -0,0 +1,46 @@ +/* + * 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 + +/// Binary Image referenced in frames from `DDThread`. +public struct BinaryImage: Codable, PassthroughAnyCodable { + public let libraryName: String + public let uuid: String + public let architecture: String + public let isSystemLibrary: Bool + public let loadAddress: String + public let maxAddress: String + + public init( + libraryName: String, + uuid: String, + architecture: String, + isSystemLibrary: Bool, + loadAddress: String, + maxAddress: String + ) { + self.libraryName = libraryName + self.uuid = uuid + self.architecture = architecture + self.isSystemLibrary = isSystemLibrary + self.loadAddress = loadAddress + self.maxAddress = maxAddress + } + + // MARK: - Encoding + + enum CodingKeys: String, CodingKey { + case libraryName = "name" + case uuid = "uuid" + case architecture = "arch" + case isSystemLibrary = "is_system" + case loadAddress = "load_address" + case maxAddress = "max_address" + } +} + +extension BinaryImage: Equatable {} diff --git a/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift new file mode 100644 index 0000000000..b3bd9a90ff --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift @@ -0,0 +1,105 @@ +/* + * 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 + +/// Crash Report format supported by Datadog SDK. +public struct DDCrashReport: Codable, PassthroughAnyCodable { + /// Meta information about the process. + /// Ref.: https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report + public struct Meta: Codable, PassthroughAnyCodable { + /// A client-generated 16-byte UUID of the incident. + public let incidentIdentifier: String? + /// The name of the crashed process. + public let process: String? + /// Parent process information. + public let parentProcess: String? + /// The location of the executable on disk. + public let path: String? + /// The CPU architecture of the process that crashed. + public let codeType: String? + /// The name of the corresponding BSD termination signal. + public let exceptionType: String? + /// CPU specific information about the exception encoded into 64-bit hexadecimal number preceded by the signal code. + public let exceptionCodes: String? + + public init( + incidentIdentifier: String?, + process: String?, + parentProcess: String?, + path: String?, + codeType: String?, + exceptionType: String?, + exceptionCodes: String? + ) { + self.incidentIdentifier = incidentIdentifier + self.process = process + self.parentProcess = parentProcess + self.path = path + self.codeType = codeType + self.exceptionType = exceptionType + self.exceptionCodes = exceptionCodes + } + + enum CodingKeys: String, CodingKey { + case incidentIdentifier = "incident_identifier" + case process = "process" + case parentProcess = "parent_process" + case path = "path" + case codeType = "code_type" + case exceptionType = "exception_type" + case exceptionCodes = "exception_codes" + } + } + + /// The date of the crash occurrence. + public let date: Date? + /// Crash report type - used to group similar crash reports. + /// In Datadog Error Tracking this corresponds to `error.type`. + public let type: String + /// Crash report message - if possible, it should provide additional troubleshooting information in addition to the crash type. + /// In Datadog Error Tracking this corresponds to `error.message`. + public let message: String + /// Unsymbolicated stack trace related to the crash (this can be either uncaugh exception backtrace or stack trace of the halted thread). + /// In Datadog Error Tracking this corresponds to `error.stack`. + public let stack: String + /// All threads running in the process. + public let threads: [DDThread] + /// List of binary images referenced from all stack traces. + public let binaryImages: [BinaryImage] + /// Meta information about the crash and process. + public let meta: Meta + /// If any stack trace information in `threads` was truncated due to stack trace minimization. + 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?, + type: String, + message: String, + stack: String, + threads: [DDThread], + binaryImages: [BinaryImage], + meta: Meta, + wasTruncated: Bool, + context: Data?, + additionalAttributes: [String: Encodable]? + ) { + self.date = date + self.type = type + self.message = message + self.stack = stack + self.threads = threads + self.binaryImages = binaryImages + self.meta = meta + self.wasTruncated = wasTruncated + self.context = context + self.additionalAttributes = AnyCodable(additionalAttributes) + } +} diff --git a/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift b/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift new file mode 100644 index 0000000000..4cc10a4ba8 --- /dev/null +++ b/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift @@ -0,0 +1,40 @@ +/* + * 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 + +/// Unsymbolicated stack trace of a running thread. +public struct DDThread: Codable, PassthroughAnyCodable { + /// The name of the thread, e.g. `"Thread 0"` + public let name: String + /// Unsymbolicated stack trace of the crash. + public let stack: String + /// If the thread was halted. + public var crashed: Bool + /// Thread state (CPU registers dump), only available for halted thread. + public let state: String? + + public init( + name: String, + stack: String, + crashed: Bool, + state: String? + ) { + self.name = name + self.stack = stack + self.crashed = crashed + self.state = state + } + + // MARK: - Encoding + + enum CodingKeys: String, CodingKey { + case name = "name" + case stack = "stack" + case crashed = "crashed" + case state = "state" + } +} 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/RUM/GlobalRUMAttributes.swift b/DatadogInternal/Sources/Models/RUM/GlobalRUMAttributes.swift new file mode 100644 index 0000000000..a4e03fdcaa --- /dev/null +++ b/DatadogInternal/Sources/Models/RUM/GlobalRUMAttributes.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 Foundation + +public struct GlobalRUMAttributes: Codable, PassthroughAnyCodable { + public let attributes: [AttributeKey: AttributeValue] + + public init(attributes: [AttributeKey: AttributeValue]) { + self.attributes = attributes + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: DynamicCodingKey.self) + try attributes.forEach { + try container.encode(AnyEncodable($1), forKey: DynamicCodingKey($0)) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKey.self) + attributes = try container.allKeys + .reduce(into: [:]) { acc, next in acc[next.stringValue] = try container.decode(AnyCodable.self, forKey: next) } + } +} diff --git a/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift b/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.swift new file mode 100644 index 0000000000..5dc8ef3a56 --- /dev/null +++ b/DatadogInternal/Sources/Models/SessionReplay/SessionReplayConfiguration.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 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. + case allow + + /// Mask all content. + case mask + + /// Mask input elements, but record all other content. + 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 +/// when enabling Session Replay. If available, the configuration can be retreived +/// with: +/// +/// let sessionReplay = core.feature( +/// named: "session-replay", +/// type: SessionReplayConfiguration.self +/// ) +/// +public protocol SessionReplayConfiguration { + /// 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 { + public static var name: String { SessionReplayFeaturneName } +} diff --git a/DatadogInternal/Sources/Models/WebViewTracking/WebViewMessage.swift b/DatadogInternal/Sources/Models/WebViewTracking/WebViewMessage.swift new file mode 100644 index 0000000000..101b2e7b6c --- /dev/null +++ b/DatadogInternal/Sources/Models/WebViewTracking/WebViewMessage.swift @@ -0,0 +1,78 @@ +/* + * 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 web-view message is transmitted by the `DatadogWebViewTracking` module +/// on the message-bus. +/// +/// Such message is decoded from Browser SDK events sent over the JS bridge. +public enum WebViewMessage { + /// The Browser event types that can be transmitted over the bridge. + public enum EventType: String, Decodable { + case log + case rum + case view + case action + case resource + case error + case longTask = "long_task" + case record + case telemetry = "internal_telemetry" + } + + /// Raw event dictionary. + public typealias Event = [String: Any] + + public struct View: Decodable { + public let id: String + } + + /// A browser log event. + case log(Event) + /// A browser rum event. + case rum(Event) + /// A browser telemetry event. + case telemetry(Event) + /// A browser session-replay record. + case record(Event, View) +} + +extension WebViewMessage: Decodable { + enum CodingKeys: CodingKey { + case eventType + case event + case view + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let eventType = try container.decode(EventType.self, forKey: .eventType) + let event = try container.decode(AnyDecodable.self, forKey: .event) + + guard let event = event.value as? Event else { + throw DecodingError.typeMismatch( + Event.self, + DecodingError.Context( + codingPath: [CodingKeys.event], + debugDescription: "The Browser Record event is not a dictionary" + ) + ) + } + + switch eventType { + case .log: + self = .log(event) + case .rum, .view, .action, .resource, .error, .longTask: + self = .rum(event) + case .telemetry: + self = .telemetry(event) + case .record: + let view = try container.decode(View.self, forKey: .view) + self = .record(event, view) + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeaders.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeaders.swift new file mode 100644 index 0000000000..0d2886a2ab --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeaders.swift @@ -0,0 +1,53 @@ +/* + * 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 + +@available(*, deprecated, renamed: "B3HTTPHeaders") +public typealias OTelHTTPHeaders = B3HTTPHeaders + +/// B3 propagation headers as explained in +/// https://github.com/openzipkin/b3-propagation/blob/master/RATIONALE.md +public enum B3HTTPHeaders { + public enum Multiple { + /// The `X-B3-TraceId` header is encoded as 32 or 16 lower-hex characters. + /// For example, a 128-bit TraceId header might look like: `X-B3-TraceId: 463ac35c9f6413ad48485a3953bb6124`. + /// Unless propagating only the Sampling State, the `X-B3-TraceId` header is required. + /// Currently we support 64-bit only. + public static let traceIDField = "X-B3-TraceId" + + /// The `X-B3-SpanId` header is encoded as 16 lower-hex characters. + /// For example, a SpanId header might look like: `X-B3-SpanId: a2fb4a1d1a96d312`. + /// Unless propagating only the Sampling State, the `X-B3-SpanId` header is required. + public static let spanIDField = "X-B3-SpanId" + + /// The `X-B3-ParentSpanId` header may be present on a child span and must be absent on the root span. + /// It is encoded as 16 lower-hex characters. + /// For example, a ParentSpanId header might look like: `X-B3-ParentSpanId: 0020000000000001`. + public static let parentSpanIDField = "X-B3-ParentSpanId" + + /// An accept sampling decision is encoded as `X-B3-Sampled: 1` and a deny as `X-B3-Sampled: 0`. + /// Absent means defer the decision to the receiver of this header. + /// For example, a Sampled header might look like: `X-B3-Sampled: 1`. + /// + /// **Note:** Before this specification was written, some tracers propagated `X-B3-Sampled` as true or false as opposed to 1 or 0. + /// While you shouldn't encode `X-B3-Sampled` as true or false, a lenient implementation may accept them. + public static let sampledField = "X-B3-Sampled" + } + + public enum Single { + /// A single header named b3 standardized in late 2018 for use in JMS and w3c `tracestate`. + /// In simplest terms b3 maps propagation fields into a hyphen delimited string. + /// `b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}`, where the last two fields are optional. + public static let b3Field = "b3" + } + + public enum Constants { + public static let sampledValue = "1" + public static let unsampledValue = "0" + public static let b3Separator = "-" + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift new file mode 100644 index 0000000000..58a9fea544 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift @@ -0,0 +1,58 @@ +/* + * 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 + +@available(*, deprecated, renamed: "B3HTTPHeadersReader") +public typealias OTelHTTPHeadersReader = B3HTTPHeadersReader + +public class B3HTTPHeadersReader: TracePropagationHeadersReader { + private let httpHeaderFields: [String: String] + + public init(httpHeaderFields: [String: String]) { + self.httpHeaderFields = httpHeaderFields + } + + public func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { + if let traceIDValue = httpHeaderFields[B3HTTPHeaders.Multiple.traceIDField], + let spanIDValue = httpHeaderFields[B3HTTPHeaders.Multiple.spanIDField], + let traceID = TraceID(traceIDValue, representation: .hexadecimal), + let spanID = SpanID(spanIDValue, representation: .hexadecimal) { + return ( + traceID: traceID, + spanID: spanID, + parentSpanID: httpHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] + .flatMap { SpanID($0, representation: .hexadecimal) } + ) + } + + let b3Value = httpHeaderFields[B3HTTPHeaders.Single.b3Field]? + .components(separatedBy: B3HTTPHeaders.Constants.b3Separator) + + if let traceIDValue = b3Value?[safe: 0], + let spanIDValue = b3Value?[safe: 1], + let traceID = TraceID(traceIDValue, representation: .hexadecimal), + let spanID = SpanID(spanIDValue, representation: .hexadecimal) { + return ( + traceID: traceID, + spanID: spanID, + parentSpanID: b3Value?[safe: 3].flatMap({ SpanID($0, representation: .hexadecimal) }) + ) + } + + return nil + } + + public var sampled: Bool? { + if let single = httpHeaderFields[B3HTTPHeaders.Single.b3Field] { + return single != B3HTTPHeaders.Constants.unsampledValue + } else if let multiple = httpHeaderFields[B3HTTPHeaders.Multiple.sampledField] { + return multiple == B3HTTPHeaders.Constants.sampledValue + } + + return nil + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift new file mode 100644 index 0000000000..6f1cd301f9 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift @@ -0,0 +1,153 @@ +/* + * 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 + +@available(*, deprecated, renamed: "B3HTTPHeadersWriter") +public typealias OTelHTTPHeadersWriter = B3HTTPHeadersWriter + +/// The `B3HTTPHeadersWriter` class facilitates the injection of trace propagation headers into network requests +/// targeted at a backend expecting [B3 propagation format](https://github.com/openzipkin/b3-propagation). +/// +/// Usage: +/// +/// var request = URLRequest(...) +/// +/// let writer = B3HTTPHeadersWriter(injectEncoding: .single) +/// let span = Tracer.shared().startRootSpan(operationName: "network request") +/// Tracer.shared().inject(spanContext: span.context, writer: writer) +/// +/// writer.traceHeaderFields.forEach { (field, value) in +/// request.setValue(value, forHTTPHeaderField: field) +/// } +/// +/// // call span.finish() when the request completes +/// +public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { + /// Enumerates B3 header encoding options. + /// + /// There are two encodings of B3 propagation: + /// [Single Header](https://github.com/openzipkin/b3-propagation#single-header) + /// and [Multiple Header](https://github.com/openzipkin/b3-propagation#multiple-headers). + /// + /// Multiple header encoding employs an `X-B3-` prefixed header per item in the trace context. + /// Single header delimits the context into a single entry named `B3`. + /// The single-header variant takes precedence over the multiple header one when extracting fields. + public enum InjectEncoding { + /// Encoding that employs `X-B3-*` prefixed headers per item in the trace context. + /// + /// See: [Multiple Header](https://github.com/openzipkin/b3-propagation#multiple-headers). + case multiple + /// Encoding that uses a single `B3` header to transport the trace context. + /// + /// See: [Single Header](https://github.com/openzipkin/b3-propagation#single-header) + case single + } + + /// A dictionary containing the required HTTP Headers for propagating trace information. + /// + /// Usage: + /// + /// writer.traceHeaderFields.forEach { (field, value) in + /// request.setValue(value, forHTTPHeaderField: field) + /// } + /// + public private(set) var traceHeaderFields: [String: String] = [:] + + private let samplingStrategy: TraceSamplingStrategy + + /// Defines whether the trace context should be injected into all requests or only sampled ones. + private let traceContextInjection: TraceContextInjection + + /// The telemetry header encoding used by the writer. + private let injectEncoding: InjectEncoding + + /// Initializes the headers writer. + /// + /// - Parameter samplingRate: The sampling rate applied for headers injection. + /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init( + samplingRate: Float, + injectEncoding: InjectEncoding = .single + ) { + self.init(sampleRate: samplingRate, injectEncoding: injectEncoding) + } + + /// Initializes the headers writer. + /// + /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. + /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init( + sampleRate: Float = 20, + injectEncoding: InjectEncoding = .single + ) { + self.init( + samplingStrategy: .custom(sampleRate: sampleRate), + injectEncoding: injectEncoding, + traceContextInjection: .all + ) + } + + /// Initializes the headers writer. + /// + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. + /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. + /// - Parameter traceContextInjection: The trace context injection strategy, with `.all` as the default. + public init( + samplingStrategy: TraceSamplingStrategy, + injectEncoding: InjectEncoding = .single, + traceContextInjection: TraceContextInjection = .all + ) { + self.samplingStrategy = samplingStrategy + self.injectEncoding = injectEncoding + self.traceContextInjection = traceContextInjection + } + + /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. + /// + /// - Parameter traceID: The trace ID. + /// - Parameter spanID: The span ID. + /// - Parameter parentSpanID: The parent span ID, if applicable. + public func write(traceContext: TraceContext) { + let sampler = samplingStrategy.sampler(for: traceContext) + let sampled = sampler.sample() + + typealias Constants = B3HTTPHeaders.Constants + + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + switch injectEncoding { + case .multiple: + traceHeaderFields = [ + B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue + ] + + if sampled { + traceHeaderFields[B3HTTPHeaders.Multiple.traceIDField] = String(traceContext.traceID, representation: .hexadecimal32Chars) + traceHeaderFields[B3HTTPHeaders.Multiple.spanIDField] = String(traceContext.spanID, representation: .hexadecimal16Chars) + traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + } + case .single: + if sampled { + traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue, + traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + ] + .compactMap { $0 } + .joined(separator: Constants.b3Separator) + } else { + traceHeaderFields[B3HTTPHeaders.Single.b3Field] = Constants.unsampledValue + } + } + case (.sampled, false): + break + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift new file mode 100644 index 0000000000..4a5b578265 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift @@ -0,0 +1,53 @@ +/* + * 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 + +public class HTTPHeadersReader: TracePropagationHeadersReader { + private let httpHeaderFields: [String: String] + + public init(httpHeaderFields: [String: String]) { + self.httpHeaderFields = httpHeaderFields + } + + public func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { + guard let traceIDLoValue = httpHeaderFields[TracingHTTPHeaders.traceIDField], + let spanIDValue = httpHeaderFields[TracingHTTPHeaders.parentSpanIDField], + let spanID = SpanID(spanIDValue, representation: .decimal) + else { + return nil + } + + // tags are comma separated key=value pairs + let tags = httpHeaderFields[TracingHTTPHeaders.tagsField]?.split(separator: ",") + .map { $0.split(separator: "=") } + .reduce(into: [String: String]()) { result, pair in + if pair.count == 2 { + result[String(pair[0])] = String(pair[1]) + } + } ?? [:] + + let traceIDHiValue = tags[TracingHTTPHeaders.TagKeys.traceIDHi] ?? "0" + + let traceID = TraceID( + idHi: UInt64(traceIDHiValue, radix: 16) ?? 0, + idLo: UInt64(traceIDLoValue, radix: 10) ?? 0 + ) + + return ( + traceID: traceID, + spanID: spanID, + parentSpanID: nil + ) + } + + public var sampled: Bool? { + if let sampling = httpHeaderFields[TracingHTTPHeaders.samplingPriorityField] { + return sampling == "1" + } + return nil + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift new file mode 100644 index 0000000000..1d38b2e43f --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.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 Foundation + +/// The `HTTPHeadersWriter` class facilitates the injection of trace propagation headers into network requests +/// targeted at a backend instrumented with Datadog and expecting `x-datadog-*` headers. +/// +/// Usage: +/// +/// var request = URLRequest(...) +/// +/// let writer = HTTPHeadersWriter() +/// let span = Tracer.shared().startRootSpan(operationName: "network request") +/// Tracer.shared().inject(spanContext: span.context, writer: writer) +/// +/// writer.traceHeaderFields.forEach { (field, value) in +/// request.setValue(value, forHTTPHeaderField: field) +/// } +/// +/// // call span.finish() when the request completes +/// +public class HTTPHeadersWriter: TracePropagationHeadersWriter { + /// A dictionary containing the required HTTP Headers for propagating trace information. + /// + /// Usage: + /// + /// writer.traceHeaderFields.forEach { (field, value) in + /// request.setValue(value, forHTTPHeaderField: field) + /// } + /// + public private(set) var traceHeaderFields: [String: String] = [:] + + private let samplingStrategy: TraceSamplingStrategy + private let traceContextInjection: TraceContextInjection + + /// Initializes the headers writer. + /// + /// - Parameter samplingRate: The sampling rate applied for headers injection. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(samplingRate: Float) { + self.init(sampleRate: samplingRate) + } + + /// Initializes the headers writer. + /// + /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(sampleRate: Float = 20) { + self.init(samplingStrategy: .custom(sampleRate: sampleRate), traceContextInjection: .all) + } + + /// Initializes the headers writer. + /// + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. + /// - Parameter traceContextInjection: The strategy for injecting trace context into requests. + public init( + samplingStrategy: TraceSamplingStrategy, + traceContextInjection: TraceContextInjection + ) { + self.samplingStrategy = samplingStrategy + self.traceContextInjection = traceContextInjection + } + + /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. + /// + /// - Parameter traceID: The trace ID. + /// - Parameter spanID: The span ID. + /// - Parameter parentSpanID: The parent span ID, if applicable. + public func write(traceContext: TraceContext) { + let sampler = samplingStrategy.sampler(for: traceContext) + let sampled = sampler.sample() + + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + traceHeaderFields = [ + TracingHTTPHeaders.samplingPriorityField: sampled ? "1" : "0" + ] + traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo) + traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal) + traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)" + case (.sampled, false): + break + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/TracingHTTPHeaders.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/TracingHTTPHeaders.swift new file mode 100644 index 0000000000..89c0f66294 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/TracingHTTPHeaders.swift @@ -0,0 +1,38 @@ +/* + * 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 + +/// Trace propagation headers as explained in +/// https://docs.datadoghq.com/real_user_monitoring/connect_rum_and_traces/?tab=browserrum#how-are-rum-resources-linked-to-traces +public enum TracingHTTPHeaders { + /// Trace propagation header. + /// It is used both in Tracing and RUM features. + public static let traceIDField = "x-datadog-trace-id" + + /// Trace propagation header. + /// In RUM - it allows Datadog to generate the first span from the trace. + /// In Tracing - it injects the `spanID` of mobile span so downstream spans can be properly linked in distributed tracing. + public static let parentSpanIDField = "x-datadog-parent-id" + + /// To make sure that the Agent keeps the trace. + /// It is used both in Tracing and RUM features. + public static let samplingPriorityField = "x-datadog-sampling-priority" + + /// The Datadog origin of the Trace. + /// + /// Setting the value to 'rum' will indicate that the span is reported as a RUM Resource. + public static let originField = "x-datadog-origin" + + /// The Datadog tags of the Trace. + public static let tagsField = "x-datadog-tags" + + /// Keys for Datadog tags. + public enum TagKeys { + /// The Datadog tag key for the higher order 64 bits of the trace ID. + public static let traceIDHi = "_dd.p.tid" + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift new file mode 100644 index 0000000000..91aaf0e760 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift @@ -0,0 +1,43 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// An interface for processing `URLSession` task interceptions. +public protocol DatadogURLSessionHandler { + /// The first party hosts configured for this handler. + var firstPartyHosts: FirstPartyHosts { get } + + /// Modifies the provided request by injecting trace headers. + /// + /// - Parameters: + /// - request: The request to be modified. + /// - headerTypes: The types of tracing headers to inject into the request. + /// - Returns: A tuple containing the modified request and the injected TraceContext. If no trace is injected (e.g., due to sampling), + /// the returned request remains unmodified, and the trace context will be nil. + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) + + /// Notifies the handler that the interception has started. + /// + /// - Parameter interception: The URLSession task interception. + func interceptionDidStart(interception: URLSessionTaskInterception) + + /// Notifies the handler that the interception has completed. + /// + /// - Parameter interception: The URLSession task interception. + func interceptionDidComplete(interception: URLSessionTaskInterception) +} + +extension DatadogCoreProtocol { + /// Core extension for registering `URLSession` handlers. + /// + /// - Parameter urlSessionHandler: The `URLSession` handler to register. + public func register(urlSessionHandler: DatadogURLSessionHandler) throws { + let feature = get(feature: NetworkInstrumentationFeature.self) ?? .init() + feature.handlers.append(urlSessionHandler) + try register(feature: feature) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift b/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift new file mode 100644 index 0000000000..a23b700b8b --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/FirstPartyHosts.swift @@ -0,0 +1,109 @@ +/* + * 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 struct that represents a dictionary of host names and tracing header types. +public struct FirstPartyHosts: Equatable { + internal var hostsWithTracingHeaderTypes: [String: Set] + + public var hosts: Set { + return Set(hostsWithTracingHeaderTypes.keys) + } + + /// Creates a `FirstPartyHosts` instance with the given dictionary of host names and tracing header types. + /// + /// - Parameter hostsWithTracingHeaderTypes: The dictionary of host names and tracing header types. + public init(_ hostsWithTracingHeaderTypes: [String: Set]) { + self.init(hostsWithTracingHeaderTypes: hostsWithTracingHeaderTypes) + } + + /// Creates a `FirstPartyHosts` instance with the given set of host names by assigning `.datadog` and `.tracecontext` header types to each. + /// + /// - Parameter hosts: The set of host names. + public init(_ hosts: Set) { + self.init( + hostsWithTracingHeaderTypes: hosts.reduce(into: [:], { partialResult, host in + partialResult[host] = [.datadog, .tracecontext] + }) + ) + } + + /// Creates empty (no hosts) `FirstPartyHosts`. + public init() { + self.init(hostsWithTracingHeaderTypes: [:]) + } + + internal init?(firstPartyHosts: URLSessionInstrumentation.FirstPartyHostsTracing?) { + switch firstPartyHosts { + case .trace(let hosts): + self.init(hosts) + case .traceWithHeaders(let hostsWithHeaders): + self.init(hostsWithTracingHeaderTypes: hostsWithHeaders) + default: + return nil + } + } + + internal init( + hostsWithTracingHeaderTypes: [String: Set], + hostsSanitizer: HostsSanitizing = HostsSanitizer() + ) { + self.hostsWithTracingHeaderTypes = hostsSanitizer.sanitized( + hostsWithTracingHeaderTypes: hostsWithTracingHeaderTypes, + warningMessage: "The first party host with header types configured for Datadog SDK is not valid" + ) + } + + /// The function takes a `URL` and returns a `Set` of matching values. + /// If one than more match is found it will return union of matching values. + public func tracingHeaderTypes(for url: URL?) -> Set { + return hostsWithTracingHeaderTypes.compactMap { item -> Set? in + let regex = "^(.*\\.)*\(NSRegularExpression.escapedPattern(for: item.key))$" + if url?.host?.range(of: regex, options: .regularExpression) != nil { + return item.value + } + return nil + } + .reduce(into: Set(), { partialResult, value in + partialResult.formUnion(value) + }) + } + + /// Returns `true` if given `URL` matches the first party hosts defined by the user; `false` otherwise. + public func isFirstParty(url: URL?) -> Bool { + return !tracingHeaderTypes(for: url).isEmpty + } + + // Returns `true` if given `String` can be parsed as a URL and matches the first + // party hosts defined by the user; `false` otherwise + public func isFirstParty(string: String) -> Bool { + guard let url = URL(string: string) else { + return false + } + return isFirstParty(url: url) + } +} + +public func += (left: inout FirstPartyHosts?, right: FirstPartyHosts) { + left = FirstPartyHosts( + left?.hostsWithTracingHeaderTypes.merging(right.hostsWithTracingHeaderTypes, uniquingKeysWith: { left, right in + left.union(right) + }) ?? right.hostsWithTracingHeaderTypes + ) +} + +public func + (left: FirstPartyHosts, right: FirstPartyHosts?) -> FirstPartyHosts { + guard let right = right else { + return left + } + + return FirstPartyHosts( + left.hostsWithTracingHeaderTypes.merging(right.hostsWithTracingHeaderTypes, uniquingKeysWith: { left, right in + left.union(right) + }) + ) +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift b/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift new file mode 100644 index 0000000000..ba2e50e30c --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/HostsSanitizer.swift @@ -0,0 +1,99 @@ +/* + * 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 + +public protocol HostsSanitizing { + func sanitized(hosts: Set, warningMessage: String) -> Set + func sanitized( + hostsWithTracingHeaderTypes: [String: Set], + warningMessage: String + ) -> [String: Set] +} + +public struct HostsSanitizer: HostsSanitizing { + private let urlRegex = #"^(http|https)://(.*)"# + private let hostRegex = #"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$"# + private let ipRegex = #"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"# + + public init() { } + + private func sanitize(host: String, warningMessage: String) -> (String?, String?) { + if host.range(of: urlRegex, options: .regularExpression) != nil { + // if an URL is given instead of the host, take its `host` part + if let sanitizedHost = URL(string: host)?.host { + let warning = "'\(host)' is an url and will be sanitized to: '\(sanitizedHost)'." + return (sanitizedHost, warning) + } else { + // otherwise, drop + let warning = "'\(host)' is not a valid host name and will be dropped." + return (nil, warning) + } + } else if host.range(of: hostRegex, options: .regularExpression) != nil { + // if a valid host name is given, accept it + return (host, nil) + } else if host.range(of: ipRegex, options: .regularExpression) != nil { + // if a valid IP address is given, accept it + return (host, nil) + } else if host == "localhost" { + // if "localhost" given, accept it + return (host, nil) + } else { + // otherwise, drop + let warning = "'\(host)' is not a valid host name and will be dropped." + return (nil, warning) + } + } + + private func printWarnings(_ warningMessage: String, _ warnings: [String]) { + warnings.forEach { warning in + consolePrint( + """ + ⚠️ \(warningMessage): \(warning) + """, + .warn + ) + } + } + + public func sanitized(hosts: Set, warningMessage: String) -> Set { + var warnings: [String] = [] + + let array: [String] = hosts.compactMap { host in + let (sanitizedHost, warning) = sanitize(host: host, warningMessage: warningMessage) + if let warning = warning { + warnings.append(warning) + } + return sanitizedHost + } + + printWarnings(warningMessage, warnings) + + return Set(array) + } + + public func sanitized( + hostsWithTracingHeaderTypes: [String: Set], + warningMessage: String + ) -> [String: Set] { + var warnings: [String] = [] + + let sanitized: [String: Set] = hostsWithTracingHeaderTypes.reduce(into: [:]) { partialResult, item in + let host = item.key + let (sanitizedHost, warning) = sanitize(host: host, warningMessage: warningMessage) + if let warning = warning { + warnings.append(warning) + } + if let sanitizedHost = sanitizedHost { + partialResult[sanitizedHost] = item.value + } + } + + printWarnings(warningMessage, warnings) + + return sanitized + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift new file mode 100644 index 0000000000..4ddaedbe3f --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift @@ -0,0 +1,280 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// The Network Instrumentation Feature that can be registered into a core if +/// any hander is provided. +/// +/// Usage: +/// +/// let core: DatadogCoreProtocol +/// +/// let handler: DatadogURLSessionHandler = CustomURLSessionHandler() +/// core.register(urlSessionInterceptor: handler) +/// +/// Registering multiple interceptor will aggregate instrumentation. +internal final class NetworkInstrumentationFeature: DatadogFeature { + /// The Feature name: "trace-propagation". + static let name = "network-instrumentation" + + /// Network Instrumentation serial queue for safe and serialized access to the + /// `URLSessionTask` interceptions. + private let queue = DispatchQueue( + label: "com.datadoghq.network-instrumentation", + target: .global(qos: .utility) + ) + + /// A no-op message bus receiver. + internal let messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() + + /// The list of registered handlers. + /// + /// Accessing this list will acquire a read-write lock for fast read operation when mutating + /// a `URLRequest` + @ReadWriteLock + internal var handlers: [DatadogURLSessionHandler] = [] + + @ReadWriteLock + private var swizzlers: [ObjectIdentifier: NetworkInstrumentationSwizzler] = [:] + + /// Maps `URLSessionTask` to its `TaskInterception` object. + /// + /// The interceptions **must** be accessed using the `queue`. + private var interceptions: [URLSessionTask: URLSessionTaskInterception] = [:] + + /// Swizzles `URLSessionTaskDelegate`, `URLSessionDataDelegate`, and `URLSessionTask` methods + /// to intercept `URLSessionTask` lifecycles. + /// + /// - Parameter configuration: The configuration to use for swizzling. + /// Note: We are only concerned with type of the delegate here but to provide compile time safety, we + /// use the instance of the delegate to get the type. + internal func bind(configuration: URLSessionInstrumentation.Configuration) throws { + let configuredFirstPartyHosts = FirstPartyHosts(firstPartyHosts: configuration.firstPartyHostsTracing) ?? .init() + + let identifier = ObjectIdentifier(configuration.delegateClass) + + if let swizzler = swizzlers[identifier] { + DD.logger.warn( + """ + The delegate class \(configuration.delegateClass) is already instrumented. + The previous instrumentation will be disabled in favor of the new one. + """ + ) + + swizzler.unswizzle() + } + + let swizzler = NetworkInstrumentationSwizzler() + swizzlers[identifier] = swizzler + + try swizzler.swizzle( + interceptResume: { [weak self] task in + // intercept task if delegate match + guard let self = self, task.dd.delegate?.isKind(of: configuration.delegateClass) == true else { + return + } + + var injectedTraceContexts: [TraceContext]? + + if let currentRequest = task.currentRequest { + let (request, traceContexts) = self.intercept(request: currentRequest, additionalFirstPartyHosts: configuredFirstPartyHosts) + task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts + } + + self.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: configuredFirstPartyHosts) + } + ) + + try swizzler.swizzle( + delegateClass: configuration.delegateClass, + interceptDidFinishCollecting: { [weak self] session, task, metrics in + self?.task(task, didFinishCollecting: metrics) + + if #available(iOS 15, tvOS 15, *), !task.dd.hasCompletion { + // iOS 15 and above, didCompleteWithError is not called hence we use task state to detect task completion + // while prior to iOS 15, task state doesn't change to completed hence we use didCompleteWithError to detect task completion + self?.task(task, didCompleteWithError: task.error) + } + }, + interceptDidCompleteWithError: { [weak self] session, task, error in + self?.task(task, didCompleteWithError: error) + } + ) + + try swizzler.swizzle( + delegateClass: configuration.delegateClass, + interceptDidReceive: { [weak self] session, task, data in + self?.task(task, didReceive: data) + } + ) + + try swizzler.swizzle( + interceptCompletionHandler: { [weak self] task, _, error in + self?.task(task, didCompleteWithError: error) + }, didReceive: { [weak self] task, data in + self?.task(task, didReceive: data) + } + ) + } + + /// Unswizzles `URLSessionTaskDelegate`, `URLSessionDataDelegate`, `URLSessionTask` and `URLSession` methods + /// - Parameter delegateClass: The delegate class to unswizzle. + internal func unbind(delegateClass: URLSessionDataDelegate.Type) { + let identifier = ObjectIdentifier(delegateClass) + swizzlers.removeValue(forKey: identifier) + } +} + +extension NetworkInstrumentationFeature { + /// Intercepts the provided request by injecting trace headers based on first-party hosts configuration. + /// + /// Only requests with URLs that match the list of first-party hosts have tracing headers injected. + /// + /// - Parameters: + /// - request: The request to intercept. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + /// - Returns: A tuple containing the modified request and the list of injected TraceContexts, one or none for each handler. If no trace is injected (e.g., due to sampling), + /// the list will be empty. + func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts?) -> (URLRequest, [TraceContext]) { + let headerTypes = firstPartyHosts(with: additionalFirstPartyHosts) + .tracingHeaderTypes(for: request.url) + + guard !headerTypes.isEmpty else { + return (request, []) + } + + var request = request + var traceContexts: [TraceContext] = [] // each handler can inject distinct trace context + for handler in handlers { + let (nextRequest, nextTraceContext) = handler.modify(request: request, headerTypes: headerTypes) + request = nextRequest + if let nextTraceContext = nextTraceContext { + traceContexts.append(nextTraceContext) + } + } + + return (request, traceContexts) + } + + /// Intercepts the provided URLSession task by creating an interception object and notifying all handlers that the interception has started. + /// + /// - Parameters: + /// - task: The URLSession task to intercept. + /// - injectedTraceContexts: The list of trace contexts injected into the task's request, one or none for each handler. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + func intercept(task: URLSessionTask, with injectedTraceContexts: [TraceContext], additionalFirstPartyHosts: FirstPartyHosts?) { + // In response to https://github.com/DataDog/dd-sdk-ios/issues/1638 capture the current request object on the + // caller thread and freeze its attributes through `ImmutableRequest`. This is to avoid changing the request + // object from multiple threads: + guard let currentRequest = task.currentRequest else { + return + } + let request = ImmutableRequest(request: currentRequest) + + queue.async { [weak self] in + guard let self = self else { + return + } + + let firstPartyHosts = self.firstPartyHosts(with: additionalFirstPartyHosts) + + let interception = self.interceptions[task] ?? + URLSessionTaskInterception( + request: request, + isFirstParty: firstPartyHosts.isFirstParty(url: request.url) + ) + + interception.register(request: request) + + if let traceContext = injectedTraceContexts.first { + // ^ If multiple trace contexts were injected (one per each handler) take the first one. This mimics the implicit + // behaviour from before RUM-3470. + interception.register(trace: traceContext) + } + + if let origin = request.ddOriginHeaderValue { + interception.register(origin: origin) + } + + self.interceptions[task] = interception + self.handlers.forEach { $0.interceptionDidStart(interception: interception) } + } + } + + /// Tells the interceptors that metrics were collected for the given task. + /// + /// - Parameters: + /// - task: The task whose metrics have been collected. + /// - metrics: The collected metrics. + func task(_ task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + queue.async { [weak self] in + guard let self = self, let interception = self.interceptions[task] else { + return + } + + interception.register( + metrics: ResourceMetrics(taskMetrics: metrics) + ) + + if interception.isDone { + self.finish(task: task, interception: interception) + } + } + } + + /// Tells the interceptors that the task has received some of the expected data. + /// + /// - Parameters: + /// - task: The task that provided data. + /// - data: A data object containing the transferred data. + func task(_ task: URLSessionTask, didReceive data: Data) { + queue.async { [weak self] in + self?.interceptions[task]?.register(nextData: data) + } + } + + /// Tells the interceptors that the task did complete. + /// + /// - Parameters: + /// - task: The task that has finished transferring data. + /// - error: If an error occurred, an error object indicating how the transfer failed, otherwise NULL. + func task(_ task: URLSessionTask, didCompleteWithError error: Error?) { + queue.async { [weak self] in + guard let self = self, let interception = self.interceptions[task] else { + return + } + + interception.register( + response: task.response, + error: error + ) + + if interception.isDone { + self.finish(task: task, interception: interception) + } + } + } + + private func firstPartyHosts(with additionalFirstPartyHosts: FirstPartyHosts?) -> FirstPartyHosts { + handlers.reduce(.init()) { $0 + $1.firstPartyHosts } + additionalFirstPartyHosts + } + + private func finish(task: URLSessionTask, interception: URLSessionTaskInterception) { + handlers.forEach { $0.interceptionDidComplete(interception: interception) } + interceptions[task] = nil + } +} + +extension NetworkInstrumentationFeature: Flushable { + /// Awaits completion of all asynchronous operations. + /// + /// **blocks the caller thread** + func flush() { + queue.sync { } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/SpanID.swift b/DatadogInternal/Sources/NetworkInstrumentation/SpanID.swift new file mode 100644 index 0000000000..a3b96b339c --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/SpanID.swift @@ -0,0 +1,161 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +public struct SpanID: RawRepresentable, Equatable, Hashable { + public static let invalidId: UInt64 = 0 + public static let invalid = SpanID() + + /// The `String` representation format of a `SpanID`. + public enum Representation { + case decimal + case hexadecimal + case hexadecimal16Chars + case hexadecimal32Chars + } + + /// The unique integer (64-bit unsigned) ID of the trace containing this span. + /// - See also: [Datadog API Reference - Send Traces](https://docs.datadoghq.com/api/?lang=bash#send-traces) + public let rawValue: UInt64 + + /// Creates a new instance with the specified raw value. + /// + /// - Parameter rawValue: The raw value to use for the new instance. + public init(rawValue: UInt64) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = Self.invalidId + } + + public func toString(representation: Representation) -> String { + String(self, representation: representation) + } +} + +extension SpanID { + /// Creates a `SpanID` from a `String` representation. + /// + /// - Parameters: + /// - string: The `String` representation. + /// - representation: The representation, `.decimal` by default. + public init?(_ string: String, representation: Representation = .decimal) { + switch representation { + case .decimal: + guard let rawValue = UInt64(string) else { + return nil + } + + self.init(rawValue: rawValue) + case .hexadecimal, .hexadecimal16Chars, .hexadecimal32Chars: + guard let rawValue = UInt64(string, radix: 16) else { + return nil + } + + self.init(rawValue: rawValue) + } + } +} + +extension SpanID: ExpressibleByIntegerLiteral { + /// Creates an instance initialized to the specified integer value. + /// + /// Do not call this initializer directly. Instead, initialize a variable or + /// constant using an integer literal. For example: + /// + /// let id: SpanID = 23 + /// + /// In this example, the assignment to the `id` constant calls this integer + /// literal initializer behind the scenes. + /// + /// - Parameter value: The value to create. + public init(integerLiteral value: UInt64) { + self.init(rawValue: value) + } +} + +extension String { + /// Creates a `String` representation of a `SpanID`. + /// + /// - Parameters: + /// - spanID: The Trace ID + /// - representation: The required representation. `.decimal` by default. + public init(_ spanID: SpanID, representation: SpanID.Representation = .decimal) { + switch representation { + case .decimal: + self.init(spanID.rawValue) + case .hexadecimal: + self.init(spanID.rawValue, radix: 16) + case .hexadecimal16Chars: + self.init(format: "%016llx", spanID.rawValue) + case .hexadecimal32Chars: + self.init(format: "%032llx", spanID.rawValue) + } + } +} + +/// A `SpanID` generator interface. +public protocol SpanIDGenerator { + /// Generates a new and unique `SpanID`. + /// + /// - Returns: The generated `SpanID` + func generate() -> SpanID +} + +/// A Default `SpanID` genarator. +public struct DefaultSpanIDGenerator: SpanIDGenerator { + /// 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. + public static let defaultGenerationRange = (1...UInt64.max >> 1) + + /// The generator's range. + let range: ClosedRange + + /// Creates a default generator. + /// + /// - Parameter range: The generator's range. + public init(range: ClosedRange = Self.defaultGenerationRange) { + self.range = range + } + + /// Generates a new and unique `SpanID`. + /// + /// The Trace ID will be generated within the range. + /// + /// - Returns: The generated `SpanID` + public func generate() -> SpanID { + var rawValue: UInt64 + repeat { + rawValue = UInt64.random(in: range) + } while rawValue == SpanID.invalidId + return SpanID(rawValue: rawValue) + } +} + +extension SpanID: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + do { + let rawValue = try container.decode(UInt64.self) + self.init(rawValue: rawValue) + } catch { + let rawValue = try container.decode(String.self) + guard let spanID = SpanID(rawValue, representation: .decimal) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid SpanID format: \(rawValue)") + } + self = spanID + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift new file mode 100644 index 0000000000..c8a9316068 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.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 + +/// A context used to propagate trace through HTTP request headers. +public struct TraceContext: Equatable { + /// The unique identifier for the trace. + public let traceID: TraceID + /// The unique identifier for the span. + public let spanID: SpanID + /// The unique identifier for the parent span, if any. + public let parentSpanID: SpanID? + /// The sample rate used for injecting the span into a request. + /// + /// It is a value between `0.0` (drop) and `100.0` (keep), determined by the local or distributed trace sampler. + public let sampleRate: Float + /// Indicates whether this span was sampled or rejected by the sampler. + public let isKept: Bool + + /// Initializes a `TraceContext` instance with the provided parameters. + /// + /// - Parameters: + /// - traceID: The unique identifier for the trace. + /// - spanID: The unique identifier for the span. + /// - parentSpanID: The unique identifier for the parent span, if any. + /// - sampleRate: The sample rate used for injecting the span into a request. + /// - isKept: A boolean indicating whether this span was sampled or rejected by the sampler. + public init( + traceID: TraceID, + spanID: SpanID, + parentSpanID: SpanID?, + sampleRate: Float, + isKept: Bool + ) { + self.traceID = traceID + self.spanID = spanID + self.parentSpanID = parentSpanID + self.sampleRate = sampleRate + self.isKept = isKept + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift new file mode 100644 index 0000000000..b134a4683c --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContextInjection.swift @@ -0,0 +1,16 @@ +/* + * 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 whether the trace context should be injected into all requests or only sampled ones. +public enum TraceContextInjection: CaseIterable { + /// Injects trace context into all requests irrespective of the sampling decision. + case all + + /// Injects trace context only into sampled requests. + case sampled +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift new file mode 100644 index 0000000000..84ecd506f0 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceID.swift @@ -0,0 +1,232 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +public struct TraceID: RawRepresentable, Equatable, Hashable { + /// The `String` representation format of a `TraceID`. + public enum Representation { + case decimal + case hexadecimal + case hexadecimal16Chars + case hexadecimal32Chars + } + + /// The unique 128-bit identifier for a trace. + public var rawValue: (UInt64, UInt64) { + get { + return (idHi, idLo) + } + } + + /// Invalid trace ID with all bits set to `0`. + public static let invalidId: UInt64 = 0 + + /// Invalid trace ID. + public static let invalid = TraceID() + + /// The unique integer (64-bit unsigned) ID of the trace + /// (high 64 bits of the 128-bit trace ID). + public private(set) var idHi: UInt64 + + /// The unique integer (64-bit unsigned) ID of the trace + /// (low 64 bits of the 128-bit trace ID). + public private(set) var idLo: UInt64 + + /// The `String` representation of high 64 bits of the trace ID. + public var idHiHex: String { + return String(format: "%llx", idHi) + } + + /// The `String` representation of low 64 bits of the trace ID. + public var idLoHex: String { + return String(format: "%llx", idLo) + } + + /// Creates a new instance with the specified raw value. + /// - Parameter rawValue: Tuple of two `UInt64` values representing high and + /// low 64 bits of the trace ID. + public init(rawValue: (UInt64, UInt64)) { + self.init(idHi: rawValue.0, idLo: rawValue.1) + } + + /// Creates a new instance with the specified low 64 bits of the trace ID. + /// - Parameter idLo: The low 64 bits of the trace ID. + public init(idLo: UInt64) { + self.init(rawValue: (0, idLo)) + } + + /// Creates a new instance with the specified high and low 64 bits of the trace ID. + /// - Parameters: + /// - idHi: High 64 bits of the trace ID. + /// - idLo: Low 64 bits of the trace ID. + public init(idHi: UInt64, idLo: UInt64) { + self.idHi = idHi + self.idLo = idLo + } + + /// Creates a new instance with invalid trace ID. + public init() { + self.idHi = Self.invalidId + self.idLo = Self.invalidId + } + + /// Returns `String` representation of the trace ID. + /// - Parameter representation: The required representation. + /// - Returns: The `String` representation of the trace ID. + public func toString(representation: Representation) -> String { + String(self, representation: representation) + } +} + +extension TraceID: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(toString(representation: .hexadecimal)) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let traceID = TraceID(string, representation: .hexadecimal) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid TraceID") + } + self = traceID + } +} + +extension TraceID { + /// Creates a `TraceID` from a `String` representation. + /// + /// - Parameters: + /// - string: The `String` representation. + /// - representation: The representation, `.decimal` by default. + public init?(_ string: String, representation: Representation = .decimal) { + switch representation { + case .decimal: + guard let idLo = UInt64(string) else { + return nil + } + + self.init(idLo: idLo) + case .hexadecimal16Chars: + guard let idLo = UInt64(string, radix: 16) else { + return nil + } + + self.init(idLo: idLo) + case .hexadecimal32Chars: + guard let idLo = UInt64(string, radix: 16), let idHi = UInt64(string.prefix(16), radix: 16) else { + return nil + } + + self.init(rawValue: (idHi, idLo)) + case .hexadecimal: + if string.count > 16 && string.count <= 32 { + let strLo = string[string.index(string.endIndex, offsetBy: -16)...] + let strHi = string[string.startIndex.. TraceID +} + +/// 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 lower part of the trace ID. + public static let defaultGenerationRange = (1...UInt64.max) + + /// The generator's range. + let range: ClosedRange + + /// Creates a default generator. + /// + /// - Parameter range: The generator's range. + public init(range: ClosedRange = Self.defaultGenerationRange) { + self.range = range + } + + /// Generates a new and unique `TraceID`. + /// + /// The Trace ID will be generated within the range. + /// <32-bit unix seconds> <32 bits of zero> <64 random bits> + /// + /// - Returns: The generated `TraceID` + public func generate() -> TraceID { + var idHi: UInt64 + var idLo: UInt64 + repeat { + // 32-bit unix seconds + 32 bits of zero in decimal + let seconds = UInt32(Date().timeIntervalSince1970) + idHi = UInt64(seconds) << 32 + idLo = UInt64.random(in: range) + } while idHi == TraceID.invalidId && idLo == TraceID.invalidId + return TraceID(idHi: idHi, idLo: idLo) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift new file mode 100644 index 0000000000..9de4c9de12 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift @@ -0,0 +1,19 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Interface that defines shared responibilities of HTTP header readers. +public protocol TracePropagationHeadersReader { + func read() -> ( + traceID: TraceID, + spanID: SpanID, + parentSpanID: SpanID? + )? + + /// Indicates whether the trace was sampled based on the provided headers. + var sampled: Bool? { get } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift new file mode 100644 index 0000000000..6d02211757 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift @@ -0,0 +1,38 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Available strategies for sampling trace propagation headers. +public enum TraceSamplingStrategy { + /// Trace propagation headers will be sampled same as propagated span. + /// + /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` + /// strategy, this decision is propagated through the request context to downstream services. + case headBased + /// Trace propagation headers will be sampled independently from sampling decision in propagated span. + /// + /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace + /// in downstream services independently of sampling their parent span. + case custom(sampleRate: Float) + + internal func sampler(for traceContext: TraceContext) -> Sampling { + switch self { + case .headBased: + return DeterministicSampler(shouldSample: traceContext.isKept, samplingRate: traceContext.sampleRate) + case .custom(let sampleRate): + return DeterministicSampler(baseId: traceContext.traceID.idLo, samplingRate: sampleRate) + } + } +} + +/// Write interface for a custom carrier +public protocol TracePropagationHeadersWriter { + var traceHeaderFields: [String: String] { get } + + func write(traceContext: TraceContext) +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracingHeaderType.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracingHeaderType.swift new file mode 100644 index 0000000000..a360c187ad --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracingHeaderType.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 + +/// The type of the tracing header injected to requests. +/// +/// - `datadog` - [Datadog's `x-datadog-*` header](https://docs.datadoghq.com/real_user_monitoring/connect_rum_and_traces/?tab=browserrum#how-are-rum-resources-linked-to-traces). +/// - `b3` - Open Telemetry B3 [Single header](https://github.com/openzipkin/b3-propagation#single-headers). +/// - `b3multi` - Open Telemetry B3 [Multiple headers](https://github.com/openzipkin/b3-propagation#multiple-headers). +/// - `tracecontext` - W3C [Trace Context header](https://www.w3.org/TR/trace-context/#tracestate-header) +public enum TracingHeaderType: Hashable { + case datadog + case b3 + case b3multi + case tracecontext +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift new file mode 100644 index 0000000000..0fbe701f6d --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift @@ -0,0 +1,161 @@ +/* + * 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 + +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") +public typealias DDURLSessionDelegate = DatadogURLSessionDelegate + +/// An interface for forwarding `URLSessionDelegate` calls to `DDURLSessionDelegate`. +/// The implementation must ensure that required methods are called on the `ddURLSessionDelegate`. +@objc +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") +public protocol __URLSessionDelegateProviding: URLSessionDelegate { + /// Datadog delegate object. + /// + /// The class implementing `DDURLSessionDelegateProviding` must ensure that following method calls are forwarded to `ddURLSessionDelegate`: + /// - `func urlSession(_:task:didFinishCollecting:)` + /// - `func urlSession(_:task:didCompleteWithError:)` + /// - `func urlSession(_:dataTask:didReceive:)` + var ddURLSessionDelegate: DatadogURLSessionDelegate { get } +} + +/// The `URLSession` delegate object which enables network requests instrumentation. **It must be +/// used together with** `DatadogRUM` or `DatadogTrace`. +/// +/// All requests made with the `URLSession` instrumented with this delegate will be intercepted by the SDK. +@objc +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") +open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { + var feature: NetworkInstrumentationFeature? { + let core = self.core ?? CoreRegistry.default + return core.get(feature: NetworkInstrumentationFeature.self) + } + + let swizzler = NetworkInstrumentationSwizzler() + + /// The instance of the SDK core notified by this delegate. + /// + /// It must be a weak reference, because `URLSessionDelegate` can last longer than core instance. + /// Any `URLSession` will retain its delegate until `.invalidateAndCancel()` is called. + private weak var core: DatadogCoreProtocol? + + @objc + override public init() { + core = nil + super.init() + swizzle(firstPartyHosts: .init()) + } + + /// Automatically tracked hosts can be customized per instance with this initializer. + /// + /// **NOTE:** If `trackURLSession(firstPartyHostsWithHeaderTypes:)` is never called, automatic tracking will **not** take place. + /// + /// - Parameter additionalFirstPartyHostsWithHeaderTypes: these hosts are tracked **in addition to** what was + /// passed to `DatadogConfiguration.Builder` via `trackURLSession(firstPartyHostsWithHeaderTypes:)` + public convenience init(additionalFirstPartyHostsWithHeaderTypes: [String: Set]) { + self.init( + in: nil, + additionalFirstPartyHostsWithHeaderTypes: additionalFirstPartyHostsWithHeaderTypes + ) + } + + /// Automatically tracked hosts can be customized per instance with this initializer. + /// + /// **NOTE:** If `trackURLSession(firstPartyHosts:)` is never called, automatic tracking will **not** take place. + /// + /// - Parameter additionalFirstPartyHosts: these hosts are tracked **in addition to** what was + /// passed to `DatadogConfiguration.Builder` via `trackURLSession(firstPartyHosts:)` + @objc + public convenience init(additionalFirstPartyHosts: Set) { + self.init( + in: nil, + additionalFirstPartyHostsWithHeaderTypes: additionalFirstPartyHosts.reduce(into: [:], { partialResult, host in + partialResult[host] = [.datadog, .tracecontext] + }) + ) + } + + /// Automatically tracked hosts can be customized per instance with this initializer. + /// + /// **NOTE:** If `trackURLSession(firstPartyHostsWithHeaderTypes:)` is never called, automatic tracking will **not** take place. + /// + /// - Parameters: + /// - core: Datadog SDK instance (or `nil` to use default SDK instance). + /// - additionalFirstPartyHosts: these hosts are tracked **in addition to** what was + /// passed to `DatadogConfiguration.Builder` via `trackURLSession(firstPartyHosts:)` + public init( + in core: DatadogCoreProtocol? = nil, + additionalFirstPartyHostsWithHeaderTypes: [String: Set] = [:] + ) { + self.core = core + super.init() + swizzle(firstPartyHosts: FirstPartyHosts(additionalFirstPartyHostsWithHeaderTypes)) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + feature?.task(task, didFinishCollecting: metrics) + if #available(iOS 15, tvOS 15, *) { + // iOS 15 and above, didCompleteWithError is not called hence we use task state to detect task completion + // while prior to iOS 15, task state doesn't change to completed hence we use didCompleteWithError to detect task completion + feature?.task(task, didCompleteWithError: task.error) + } + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + // NOTE: This delegate method is only called for `URLSessionTasks` created without the completion handler. + feature?.task(dataTask, didReceive: data) + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + // NOTE: This delegate method is only called for `URLSessionTasks` created without the completion handler. + feature?.task(task, didCompleteWithError: error) + } + + private func swizzle(firstPartyHosts: FirstPartyHosts) { + do { + try swizzler.swizzle( + interceptResume: { [weak self] task in + guard + let feature = self?.feature, + let provider = task.dd.delegate as? __URLSessionDelegateProviding, + provider.ddURLSessionDelegate === self // intercept task with self as delegate + else { + return + } + + var injectedTraceContexts: [TraceContext]? + + if let currentRequest = task.currentRequest { + let (request, traceContexts) = feature.intercept(request: currentRequest, additionalFirstPartyHosts: firstPartyHosts) + task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts + } + + feature.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: firstPartyHosts) + } + ) + + try swizzler.swizzle( + interceptCompletionHandler: { [weak self] task, _, error in + self?.feature?.task(task, didCompleteWithError: error) + }, didReceive: { _, _ in + } + ) + } catch { + DD.logger.error("Fails to apply swizzling for instrumenting \(Self.self)", error: error) + } + } + + deinit { + swizzler.unswizzle() + } +} + +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") +extension DatadogURLSessionDelegate: __URLSessionDelegateProviding { + public var ddURLSessionDelegate: DatadogURLSessionDelegate { self } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift new file mode 100644 index 0000000000..be7951f400 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/ImmutableRequest.swift @@ -0,0 +1,34 @@ +/* + * 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 + +/// An immutable version of `URLRequest`. +/// +/// Introduced in response to concerns raised in https://github.com/DataDog/dd-sdk-ios/issues/1638 +/// it makes a copy of request attributes, safeguarding against potential thread safety issues arising from concurrent +/// mutations (see more context in https://github.com/DataDog/dd-sdk-ios/pull/1767 ). +public struct ImmutableRequest { + /// The URL of the request. + public let url: URL? + /// The HTTP method of the request. + public let httpMethod: String? + /// The value of `x-datadog-origin` header (if any). + public let ddOriginHeaderValue: String? + /// A reference to the original `URLRequest` object provided during initialization. Direct use is discouraged + /// due to thread safety concerns. Instead, necessary attributes should be accessed through `ImmutableRequest` fields. + public let unsafeOriginal: URLRequest + + public init(request: URLRequest) { + self.url = request.url + self.httpMethod = request.httpMethod + // RUM-3183: As observed in https://github.com/DataDog/dd-sdk-ios/issues/1638, accessing `request.allHTTPHeaderFields` is not + // safe and can lead to crashes with undefined root cause. To avoid issues we should prefer `request.value(forHTTPHeaderField:)` + // when interacting with `URLRequest`. + self.ddOriginHeaderValue = request.value(forHTTPHeaderField: TracingHTTPHeaders.originField) + self.unsafeOriginal = request + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/NetworkInstrumentationSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/NetworkInstrumentationSwizzler.swift new file mode 100644 index 0000000000..8c6c9d6dbf --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/NetworkInstrumentationSwizzler.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 + +/// Swizzles `URLSession*` methods. +internal final class NetworkInstrumentationSwizzler { + let urlSessionSwizzler: URLSessionSwizzler + let urlSessionTaskSwizzler: URLSessionTaskSwizzler + let urlSessionTaskDelegateSwizzler: URLSessionTaskDelegateSwizzler + let urlSessionDataDelegateSwizzler: URLSessionDataDelegateSwizzler + + init() { + let lock = NSRecursiveLock() + urlSessionSwizzler = URLSessionSwizzler(lock: lock) + urlSessionTaskSwizzler = URLSessionTaskSwizzler(lock: lock) + urlSessionTaskDelegateSwizzler = URLSessionTaskDelegateSwizzler(lock: lock) + urlSessionDataDelegateSwizzler = URLSessionDataDelegateSwizzler(lock: lock) + } + + /// Swizzles `URLSession.dataTask(with:completionHandler:)` methods (with `URL` and `URLRequest`). + func swizzle( + interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void, + didReceive: @escaping (URLSessionTask, Data) -> Void + ) throws { + try urlSessionSwizzler.swizzle( + interceptCompletionHandler: interceptCompletionHandler, + didReceive: didReceive + ) + } + + /// Swizzles `URLSessionTask.resume()` method. + func swizzle( + interceptResume: @escaping (URLSessionTask) -> Void + ) throws { + try urlSessionTaskSwizzler.swizzle(interceptResume: interceptResume) + } + + /// Swizzles methods: + /// - `URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)` + /// - `URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)` + func swizzle( + delegateClass: URLSessionTaskDelegate.Type, + interceptDidFinishCollecting: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, + interceptDidCompleteWithError: @escaping (URLSession, URLSessionTask, Error?) -> Void + ) throws { + try urlSessionTaskDelegateSwizzler.swizzle( + delegateClass: delegateClass, + interceptDidFinishCollecting: interceptDidFinishCollecting, + interceptDidCompleteWithError: interceptDidCompleteWithError + ) + } + + /// Swizzles methods: + /// - `URLSessionDataDelegate.urlSession(_:dataTask:didReceive:)` + func swizzle( + delegateClass: URLSessionDataDelegate.Type, + interceptDidReceive: @escaping (URLSession, URLSessionDataTask, Data) -> Void + ) throws { + try urlSessionDataDelegateSwizzler.swizzle( + delegateClass: delegateClass, + interceptDidReceive: interceptDidReceive + ) + } + + /// Unswizzles all. + func unswizzle() { + urlSessionSwizzler.unswizzle() + urlSessionTaskSwizzler.unswizzle() + urlSessionTaskDelegateSwizzler.unswizzle() + urlSessionDataDelegateSwizzler.unswizzle() + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.swift new file mode 100644 index 0000000000..0c6ef61e94 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionDataDelegateSwizzler.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 Foundation + +/// Swizzles `URLSessionDataDelegate` callbacks. +internal class URLSessionDataDelegateSwizzler { + private let lock: NSLocking + private var didReceive: DidReceive? + + init(lock: NSLocking = NSLock()) { + self.lock = lock + } + + /// Swizzles methods: + /// - `URLSessionDataDelegate.urlSession(_:dataTask:didReceive:)` + func swizzle( + delegateClass: URLSessionDataDelegate.Type, + interceptDidReceive: @escaping (URLSession, URLSessionDataTask, Data) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + didReceive = try DidReceive.build(klass: delegateClass) + didReceive?.swizzle(intercept: interceptDidReceive) + } + + /// Unswizzles all. + /// + /// This method is called during deinit. + func unswizzle() { + lock.lock() + didReceive?.unswizzle() + lock.unlock() + } + + deinit { + unswizzle() + } + + /// Swizzles `urlSession(_:dataTask:didReceive:)` callback. + /// This callback is called when the response is received. + /// It is called multiple times for a single request, each time with a new chunk of data. + class DidReceive: MethodSwizzler<@convention(c) (URLSessionDataDelegate, Selector, URLSession, URLSessionDataTask, Data) -> Void, @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void> { + private static let selector = #selector(URLSessionDataDelegate.urlSession(_:dataTask:didReceive:)) + + private let method: Method + + static func build(klass: AnyClass) throws -> DidReceive { + return try DidReceive(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try dd_class_getInstanceMethod(klass, selector) + } catch { + // URLSessionDataDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void = { delegate, session, task, data in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try dd_class_getInstanceMethod(klass, selector) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionDataTask, Data) -> Void) { + typealias Signature = @convention(block) (URLSessionDataDelegate, URLSession, URLSessionDataTask, Data) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, data in + intercept(session, task, data) + return previousImplementation(delegate, Self.selector, session, task, data) + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift new file mode 100644 index 0000000000..02bf907129 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInstrumentation.swift @@ -0,0 +1,93 @@ +/* + * 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 + +/// An entry point to enable URLSession instrumentation. +public enum URLSessionInstrumentation { + /// Enables URLSession instrumentation. + /// + /// - Parameters: + /// - configuration: Configuration of the feature. + /// - core: The instance of Datadog SDK to enable URLSession instrumentation in (global instance by default). + public static func enable(with configuration: URLSessionInstrumentation.Configuration, in core: DatadogCoreProtocol = CoreRegistry.default) { + do { + try enableOrThrow(with: configuration, in: core) + } catch let error { + consolePrint("\(error)", .error) + + if error is InternalError { // SDK error, send to telemetry + core.telemetry.error(error) + } + } + } + + internal static func enableOrThrow(with configuration: URLSessionInstrumentation.Configuration, in core: DatadogCoreProtocol) throws { + guard let feature = core.get(feature: NetworkInstrumentationFeature.self) else { + throw ProgrammerError(description: "URLSession tracking must be enabled before enabling URLSessionInstrumentation using either RUM or Trace feature.") + } + + try feature.bind(configuration: configuration) + } + + /// Disables URLSession instrumentation. + /// - Parameters: + /// - delegateClass: The delegate class to unbind. + /// - core: The instance of Datadog SDK to disable URLSession instrumentation in (global instance by default). + public static func disable(delegateClass: URLSessionDataDelegate.Type, in core: DatadogCoreProtocol = CoreRegistry.default) { + do { + try disableOrThrow(delegateClass: delegateClass, in: core) + } catch let error { + consolePrint("\(error)", .error) + + if error is InternalError { // SDK error, send to telemetry + core.telemetry.error(error) + } + } + } + + internal static func disableOrThrow(delegateClass: URLSessionDataDelegate.Type, in core: DatadogCoreProtocol) throws { + guard let feature = core.get(feature: NetworkInstrumentationFeature.self) else { + throw ProgrammerError(description: "URLSession tracking must be enabled before enabling URLSessionInstrumentation using either RUM or Trace feature.") + } + + feature.unbind(delegateClass: delegateClass) + } +} + +extension URLSessionInstrumentation { + /// Configuration of URLSession instrumentation. + public struct Configuration { + /// The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + public var delegateClass: URLSessionDataDelegate.Type + + /// Additional first party hosts to consider in the interception. + public var firstPartyHostsTracing: FirstPartyHostsTracing? + + /// Configuration of URLSession instrumentation. + /// - Parameters: + /// - delegate: The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + /// - firstPartyHostsTracing: Additional first party hosts to consider in the interception. + public init(delegateClass: URLSessionDataDelegate.Type, firstPartyHostsTracing: FirstPartyHostsTracing? = nil) { + self.delegateClass = delegateClass + self.firstPartyHostsTracing = firstPartyHostsTracing + } + } + + /// Defines configuration for first-party hosts in distributed tracing. + public enum FirstPartyHostsTracing { + /// Trace the specified hosts using Datadog and W3C `tracecontext` tracing headers. + /// + /// - Parameters: + /// - hosts: The set of hosts to inject tracing headers. Note: Hosts must not include the "http(s)://" prefix. + case trace(hosts: Set) + + /// Trace given hosts with using custom tracing headers. + /// + /// - `hostsWithHeaders` - Dictionary of hosts and tracing header types to use. Note: Hosts must not include "http(s)://" prefix. + case traceWithHeaders(hostsWithHeaders: [String: Set]) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift new file mode 100644 index 0000000000..83c1ce819e --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift @@ -0,0 +1,116 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +// TODO: RUM-3470 Add tests to `URLSessionInterceptor` + +/// The `URLSession` Interceptor provides methods for injecting distributed-traces +/// headers into a `URLRequest`and to instrument a `URLURLSessionTask` lifcycle, +/// from its creation to completion. +/// +/// Any Feature supporting `URLSession` instrumentation will receive interceptions through +/// their `DatadogURLSessionHandler` implementation. +public struct URLSessionInterceptor { + let feature: NetworkInstrumentationFeature + + /// Returns the Interceptor registerd in core. + /// + /// This method will return an interceptor if any `DatadogURLSessionHandler` have been + /// registered in the given core. + public static func shared(in core: DatadogCoreProtocol = CoreRegistry.default) -> URLSessionInterceptor? { + guard let feature = core.get(feature: NetworkInstrumentationFeature.self) else { + return nil + } + + return URLSessionInterceptor(feature: feature) + } + + /// Maps the trace ID to the full trace context generated for that trace. + /// + /// 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 + static var contextsByTraceID: [TraceID: [TraceContext]] = [:] + + /// Tells the interceptor to modify a URL request. + /// + /// - Parameters: + /// - request: The request to intercept. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. + /// - Returns: The modified request. + public func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts? = nil) -> URLRequest { + let (request, traceContexts) = feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) + if let traceID = extractTraceID(from: request) { + URLSessionInterceptor.contextsByTraceID[traceID] = traceContexts + } + return request + } + + /// Tells the interceptors that a task was created. + /// + /// - Parameters: + /// - task: The created task. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. + public func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts? = nil) { + var injectedTraceContexts: [TraceContext] = [] + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + injectedTraceContexts = URLSessionInterceptor.contextsByTraceID[traceID] ?? [] + } + + feature.intercept(task: task, with: injectedTraceContexts, additionalFirstPartyHosts: additionalFirstPartyHosts) + } + + /// Tells the interceptor that metrics were collected for the given task. + /// + /// - Parameters: + /// - task: The task whose metrics have been collected. + /// - metrics: The collected metrics. + public func task(_ task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + feature.task(task, didFinishCollecting: metrics) + } + + /// Tells the interceptor that the task has received some of the expected data. + /// + /// - Parameters: + /// - task: The data task that provided data. + /// - data: A data object containing the transferred data. + public func task(_ task: URLSessionTask, didReceive data: Data) { + feature.task(task, didReceive: data) + } + + /// Tells the interceptor that the task did complete. + /// + /// - Parameters: + /// - task: The task that has finished transferring data. + /// - error: If an error occurred, an error object indicating how the transfer failed, otherwise NULL. + public func task(_ task: URLSessionTask, didCompleteWithError error: Error?) { + feature.task(task, didCompleteWithError: error) + + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + URLSessionInterceptor.contextsByTraceID[traceID] = nil + } + } + + // MARK: - Private + + private func extractTraceID(from request: URLRequest) -> TraceID? { + guard let headers = request.allHTTPHeaderFields else { // swiftlint:disable:this unsafe_all_http_header_fields + return nil + } + + // Try all supported header types until first one is matched: + if let dd = HTTPHeadersReader(httpHeaderFields: headers).read() { + return dd.traceID + } else if let b3 = B3HTTPHeadersReader(httpHeaderFields: headers).read() { + return b3.traceID + } else if let w3c = W3CHTTPHeadersReader(httpHeaderFields: headers).read() { + return w3c.traceID + } + + return nil + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift new file mode 100644 index 0000000000..5e9450fa8e --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.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. + */ + +import Foundation + +/// Swizzles `URLSession*` methods. +internal final class URLSessionSwizzler { + private let lock: NSLocking + private var dataTaskURLRequestCompletionHandler: DataTaskURLRequestCompletionHandler? + private var dataTaskURLCompletionHandler: DataTaskURLCompletionHandler? + + init(lock: NSLocking = NSLock()) { + self.lock = lock + } + + /// Swizzles `URLSession.dataTask(with:completionHandler:)` methods (with `URL` and `URLRequest`). + func swizzle( + interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void, + didReceive: @escaping (URLSessionTask, Data) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + dataTaskURLRequestCompletionHandler = try DataTaskURLRequestCompletionHandler.build() + dataTaskURLRequestCompletionHandler?.swizzle( + interceptCompletion: interceptCompletionHandler, + didReceive: didReceive + ) + + if #available(iOS 13.0, *) { + // Prior to iOS 13.0 the `URLSession.dataTask(with:url, completionHandler:handler)` makes an internal + // call to `URLSession.dataTask(with:request, completionHandler:handler)`. To avoid duplicated call + // to the callback, we don't apply below swizzling prior to iOS 13. + dataTaskURLCompletionHandler = try DataTaskURLCompletionHandler.build() + dataTaskURLCompletionHandler?.swizzle( + interceptCompletion: interceptCompletionHandler, + didReceive: didReceive + ) + } + } + + /// Unswizzles all. + /// + /// This method is called during deinit. + func unswizzle() { + lock.lock() + dataTaskURLRequestCompletionHandler?.unswizzle() + dataTaskURLCompletionHandler?.unswizzle() + lock.unlock() + } + + deinit { + unswizzle() + } + + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + + /// Swizzles `URLSession.dataTask(with:completionHandler:)` (with `URLRequest`) method. + class DataTaskURLRequestCompletionHandler: MethodSwizzler<@convention(c) (URLSession, Selector, URLRequest, CompletionHandler?) -> URLSessionDataTask, @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask> { + private static let selector = #selector( + URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping CompletionHandler) -> URLSessionDataTask + ) + + private let method: Method + + static func build() throws -> DataTaskURLRequestCompletionHandler { + return try DataTaskURLRequestCompletionHandler( + selector: self.selector, + klass: URLSession.self + ) + } + + private init(selector: Selector, klass: AnyClass) throws { + self.method = try dd_class_getInstanceMethod(klass, selector) + super.init() + } + + func swizzle( + interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void, + didReceive: @escaping (URLSessionTask, Data) -> Void + ) { + typealias Signature = @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in + return { session, request, completionHandler -> URLSessionDataTask in + guard let completionHandler = completionHandler else { + // The `completionHandler` can be `nil` in two cases: + // - on iOS 11 or 12, where `dataTask(with:)` (for `URL` and `URLRequest`) calls + // the `dataTask(with:completionHandler:)` (for `URLRequest`) internally by nullifying the completion block. + // - when `[session dataTaskWithURL:completionHandler:]` is called in Objective-C with explicitly passing + // `nil` as the `completionHandler` (it produces a warning, but compiles). + return previousImplementation(session, Self.selector, request, completionHandler) + } + + var _task: URLSessionDataTask? + let task = previousImplementation(session, Self.selector, request) { data, response, error in + if let task = _task, let data = data { + didReceive(task, data) + } + + if let task = _task { // sanity check, should always succeed + interceptCompletion(task, data, error) + } + + completionHandler(data, response, error) + } + _task = task + _task?.dd.hasCompletion = true + return task + } + } + } + } + + /// Swizzles `URLSession.dataTask(with:completionHandler:)` (with `URL`) method. + class DataTaskURLCompletionHandler: MethodSwizzler<@convention(c) (URLSession, Selector, URL, CompletionHandler?) -> URLSessionDataTask, @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask> { + private static let selector = #selector( + URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URL, @escaping CompletionHandler) -> URLSessionDataTask + ) + + private let method: Method + + static func build() throws -> DataTaskURLCompletionHandler { + return try DataTaskURLCompletionHandler( + selector: self.selector, + klass: URLSession.self + ) + } + + private init(selector: Selector, klass: AnyClass) throws { + self.method = try dd_class_getInstanceMethod(klass, selector) + super.init() + } + + func swizzle( + interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void, + didReceive: @escaping (URLSessionTask, Data) -> Void + ) { + typealias Signature = @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask + swizzle(method) { previousImplementation -> Signature in + return { session, url, completionHandler -> URLSessionDataTask in + guard let completionHandler = completionHandler else { + // The `completionHandler` can be `nil` in two cases: + // - on iOS 11 or 12, where `dataTask(with:)` (for `URL` and `URLRequest`) calls + // the `dataTask(with:completionHandler:)` (for `URLRequest`) internally by nullifying the completion block. + // - when `[session dataTaskWithURL:completionHandler:]` is called in Objective-C with explicitly passing + // `nil` as the `completionHandler` (it produces a warning, but compiles). + return previousImplementation(session, Self.selector, url, completionHandler) + } + + var _task: URLSessionDataTask? + let task = previousImplementation(session, Self.selector, url) { data, response, error in + if let task = _task, let data = data { + didReceive(task, data) + } + + completionHandler(data, response, error) + + if let task = _task { // sanity check, should always succeed + interceptCompletion(task, data, error) + } + } + _task = task + _task?.dd.hasCompletion = true + return task + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift new file mode 100644 index 0000000000..71b0c66f0e --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.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 + +extension URLSessionTask: DatadogExtended {} +extension DatadogExtension where ExtendedType: URLSessionTask { + /// Overrides the current request of the ``URLSessionTask``. + /// + /// The current request must be overriden before the task resumes. + /// + /// - Parameter request: The new request. + func override(currentRequest request: URLRequest) { + // The `URLSessionTask` is Key-Value Coding compliant and we can + // set the `currentRequest` property + type.setValue(request, forKey: "currentRequest") + } + + /// Returns the delegate instance the task is reporting to. + var delegate: URLSessionDelegate? { + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, *), let delegate = type.delegate { + return delegate + } + + // The `URLSessionTask` is Key-Value Coding compliant and retains a + // `session` property + guard let session = type.value(forKey: "session") as? URLSession else { + return nil + } + + return session.delegate + } + + var hasCompletion: Bool { + get { + let value = objc_getAssociatedObject(type, &hasCompletionKey) as? Bool + return value == true + } + set { + if newValue { + objc_setAssociatedObject(type, &hasCompletionKey, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } else { + objc_setAssociatedObject(type, &hasCompletionKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + } +} + +private var hasCompletionKey: Void? diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift new file mode 100644 index 0000000000..48194b9b72 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskDelegateSwizzler.swift @@ -0,0 +1,137 @@ +/* + * 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 + +/// Swizzles `URLSessionTaskDelegate` callbacks. +internal class URLSessionTaskDelegateSwizzler { + private let lock: NSLocking + private var didFinishCollecting: DidFinishCollecting? + private var didCompleteWithError: DidCompleteWithError? + + init(lock: NSLocking = NSLock()) { + self.lock = lock + } + + /// Swizzles methods: + /// - `URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)` + /// - `URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)` + func swizzle( + delegateClass: URLSessionTaskDelegate.Type, + interceptDidFinishCollecting: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, + interceptDidCompleteWithError: @escaping (URLSession, URLSessionTask, Error?) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + didFinishCollecting = try DidFinishCollecting.build(klass: delegateClass) + didCompleteWithError = try DidCompleteWithError.build(klass: delegateClass) + didFinishCollecting?.swizzle(intercept: interceptDidFinishCollecting) + didCompleteWithError?.swizzle(intercept: interceptDidCompleteWithError) + } + + /// Unswizzles all. + /// + /// This method is called during deinit. + func unswizzle() { + lock.lock() + didFinishCollecting?.unswizzle() + didCompleteWithError?.unswizzle() + lock.unlock() + } + + deinit { + unswizzle() + } + + /// Swizzles `URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)` method. + class DidFinishCollecting: MethodSwizzler<@convention(c) (URLSessionTaskDelegate, Selector, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void, @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void> { + private static let selector = #selector(URLSessionTaskDelegate.urlSession(_:task:didFinishCollecting:)) + + private let method: Method + + static func build(klass: AnyClass) throws -> DidFinishCollecting { + return try DidFinishCollecting(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try dd_class_getInstanceMethod(klass, selector) + } catch { + // URLSessionTaskDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void = { delegate, session, task, metrics in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try dd_class_getInstanceMethod(klass, selector) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void) { + typealias Signature = @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, metrics in + intercept(session, task, metrics) + return previousImplementation(delegate, Self.selector, session, task, metrics) + } + } + } + } + + class DidCompleteWithError: MethodSwizzler<@convention(c) (URLSessionTaskDelegate, Selector, URLSession, URLSessionTask, Error?) -> Void, @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void> { + private static let selector = #selector(URLSessionTaskDelegate.urlSession(_:task:didCompleteWithError:)) + + private let method: Method + + static func build(klass: AnyClass) throws -> DidCompleteWithError { + return try DidCompleteWithError(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + do { + method = try dd_class_getInstanceMethod(klass, selector) + } catch { + // URLSessionTaskDelegate doesn't implement the selector, so we inject it and swizzle it + let block: @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void = { delegate, session, task, error in + } + let imp = imp_implementationWithBlock(block) + /* + v@:@@@ means: + v - return type is void + @ - self + : - selector + @ - first argument is an object + @ - second argument is an object + @ - third argument is an object + */ + class_addMethod(klass, selector, imp, "v@:@@@") + method = try dd_class_getInstanceMethod(klass, selector) + } + + super.init() + } + + func swizzle(intercept: @escaping (URLSession, URLSessionTask, Error?) -> Void) { + typealias Signature = @convention(block) (URLSessionTaskDelegate, URLSession, URLSessionTask, Error?) -> Void + swizzle(method) { previousImplementation -> Signature in + return { delegate, session, task, error in + intercept(session, task, error) + return previousImplementation(delegate, Self.selector, session, task, error) + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift new file mode 100644 index 0000000000..17bfb3b7bb --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift @@ -0,0 +1,234 @@ +/* + * 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 + +public class URLSessionTaskInterception { + /// An identifier uniquely identifying the task interception across all `URLSessions`. + public let identifier: UUID + /// The initial request send during this interception. It is, the request send from `URLSession`, not the one + /// given by the user (as the request could have been modified in `URLSessionSwizzler`). + public private(set) var request: ImmutableRequest + /// Tells if the `request` is send to a 1st party host. + public let isFirstPartyRequest: Bool + /// Task metrics collected during this interception. + public private(set) var metrics: ResourceMetrics? + /// Task data received during this interception. Can be `nil` if task completed with error. + public private(set) var data: Data? + /// Task completion collected during this interception. + public private(set) var completion: ResourceCompletion? + /// Trace context injected to request headers. Can be `nil` if the trace was not sampled or if modifying + /// request was not possible in `URLSession` swizzling on certain OS version. + public private(set) var trace: TraceContext? + /// The Datadog origin of the Trace. + /// + /// Setting the value to 'rum' will indicate that the span is reported as a RUM Resource. + public private(set) var origin: String? + + init(request: ImmutableRequest, isFirstParty: Bool) { + self.identifier = UUID() + self.request = request + self.isFirstPartyRequest = isFirstParty + } + + func register(metrics: ResourceMetrics) { + self.metrics = metrics + } + + func register(nextData: Data) { + if data != nil { + self.data?.append(nextData) + } else { + self.data = nextData + } + } + + func register(request: ImmutableRequest) { + self.request = request + } + + func register(response: URLResponse?, error: Error?) { + self.completion = ResourceCompletion( + response: response as? HTTPURLResponse, + error: error + ) + } + + public func register(trace: TraceContext) { + self.trace = trace + } + + public func register(origin: String) { + self.origin = origin + } + + /// Tells if the interception is done (mean: both metrics and completion were collected). + public var isDone: Bool { + metrics != nil && completion != nil + } +} + +public struct ResourceCompletion { + public let httpResponse: HTTPURLResponse? + public let error: Error? + + public init(response: URLResponse?, error: Error?) { + self.httpResponse = response as? HTTPURLResponse + self.error = error + } +} + +/// Encapsulates key metrics retrieved either from `URLSessionTaskMetrics` or any other relevant data source. +/// Reference: https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics +public struct ResourceMetrics { + public struct DateInterval { + public let start, end: Date + public var duration: TimeInterval { end.timeIntervalSince(start) } + + public static func create(start: Date?, end: Date?) -> DateInterval? { + if let start = start, let end = end { + return DateInterval(start: start, end: end) + } + return nil + } + + public init(start: Date, end: Date) { + self.start = start + self.end = end + } + } + + /// Properties of the fetch phase for the resource: + /// - `start` - the time when the task started fetching the resource from the server, + /// - `end` - the time immediately after the task received the last byte of the resource. + public let fetch: DateInterval + + /// Properties of the redirection phase for the resource. If the resource is retrieved in multiple transactions, + /// only the last one is used to track detailed metrics (`dns`, `connect` etc.). + /// All but last are described as a single "redirection" phase. + public let redirection: DateInterval? + + /// Properties of the name lookup phase for the resource. + public let dns: DateInterval? + + /// Properties of the connect phase for the resource. + public let connect: DateInterval? + + /// Properties of the secure connect phase for the resource. + public let ssl: DateInterval? + + /// Properties of the TTFB phase for the resource. + public let firstByte: DateInterval? + + /// Properties of the download phase for the resource. + public let download: DateInterval? + + /// The size of data delivered to delegate or completion handler. + public let responseSize: Int64? + + public init( + fetch: DateInterval, + redirection: DateInterval?, + dns: DateInterval?, + connect: DateInterval?, + ssl: DateInterval?, + firstByte: DateInterval?, + download: DateInterval?, + responseSize: Int64? + ) { + self.fetch = fetch + self.redirection = redirection + self.dns = dns + self.connect = connect + self.ssl = ssl + self.firstByte = firstByte + self.download = download + self.responseSize = responseSize + } +} + +extension ResourceMetrics { + public init(taskMetrics: URLSessionTaskMetrics) { + let fetch = DateInterval( + start: taskMetrics.taskInterval.start, + end: taskMetrics.taskInterval.end + ) + + let transactions = taskMetrics.transactionMetrics + .filter { $0.resourceFetchType != .localCache } // ignore loads from cache + + // Note: `transactions` contain metrics for each individual + // `request → response` transaction done for given resource, e.g.: + // * if `200 OK` was received, it will contain 1 transaction, + // * if `200 OK` was preceeded by `301` redirection, it will contain 2 transactions. + let mainTransaction = transactions.last + let redirectionTransactions = transactions.dropLast() + + var redirection: DateInterval? = nil + + if redirectionTransactions.count > 0 { + let redirectionStarts = redirectionTransactions.compactMap { $0.fetchStartDate } + let redirectionEnds = redirectionTransactions.compactMap { $0.responseEndDate } + + // If several redirections were made, we model them as a single "redirection" + // phase starting in the first moment of the youngest and ending + // in the last moment of the oldest. + if let redirectionPhaseStart = redirectionStarts.first, + let redirectionPhaseEnd = redirectionEnds.last { + redirection = DateInterval(start: redirectionPhaseStart, end: redirectionPhaseEnd) + } + } + + var dns: DateInterval? = nil + var connect: DateInterval? = nil + var ssl: DateInterval? = nil + var firstByte: DateInterval? = nil + var download: DateInterval? = nil + var responseSize: Int64? = nil + + if let mainTransaction = mainTransaction { + if let dnsStart = mainTransaction.domainLookupStartDate, + let dnsEnd = mainTransaction.domainLookupEndDate { + dns = DateInterval(start: dnsStart, end: dnsEnd) + } + + if let connectStart = mainTransaction.connectStartDate, + let connectEnd = mainTransaction.connectEndDate { + connect = DateInterval(start: connectStart, end: connectEnd) + } + + if let sslStart = mainTransaction.secureConnectionStartDate, + let sslEnd = mainTransaction.secureConnectionEndDate { + ssl = DateInterval(start: sslStart, end: sslEnd) + } + + if let firstByteStart = mainTransaction.requestStartDate, // Time from start requesting the resource ... + let firstByteEnd = mainTransaction.responseStartDate { // ... to receiving the first byte of the response + firstByte = DateInterval(start: firstByteStart, end: firstByteEnd) + } + + if let downloadStart = mainTransaction.responseStartDate, // Time from the first byte of the response ... + let downloadEnd = mainTransaction.responseEndDate { // ... to receiving the last byte. + download = DateInterval(start: downloadStart, end: downloadEnd) + } + + if #available(iOS 13.0, tvOS 13, *) { + responseSize = mainTransaction.countOfResponseBodyBytesAfterDecoding + } + } + + self.init( + fetch: fetch, + redirection: redirection, + dns: dns, + connect: connect, + ssl: ssl, + firstByte: firstByte, + download: download, + responseSize: responseSize + ) + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift new file mode 100644 index 0000000000..2e84507fac --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskSwizzler.swift @@ -0,0 +1,72 @@ +/* + * 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 + +internal final class URLSessionTaskSwizzler { + private let lock: NSLocking + private var taskResume: TaskResume? + + init(lock: NSLocking = NSLock()) { + self.lock = lock + } + + /// Swizzles `URLSessionTask.resume()` method. + func swizzle( + interceptResume: @escaping (URLSessionTask) -> Void + ) throws { + lock.lock() + defer { lock.unlock() } + taskResume = try TaskResume.build() + taskResume?.swizzle(intercept: interceptResume) + } + + /// Unswizzles all. + /// + /// This method is called during deinit. + func unswizzle() { + lock.lock() + taskResume?.unswizzle() + lock.unlock() + } + + deinit { + unswizzle() + } + + /// Swizzles `URLSessionTask.resume()` method. + class TaskResume: MethodSwizzler<@convention(c) (URLSessionTask, Selector) -> Void, @convention(block) (URLSessionTask) -> Void> { + private static let selector = #selector(URLSessionTask.resume) + + private let method: Method + + static func build() throws -> TaskResume { + // RUM-2690: We swizzle private `__NSCFLocalSessionTask` class as it appears to be uniformly used + // in iOS versions 12.x - 17.x. Swizzling the public `URLSessionTask.resume()` doesn't work in 12.x and 13.x. + // See https://github.com/DataDog/dd-sdk-ios/pull/1637 for full `URLSessionTask` class dumps in major iOS versions. + let className = "__NSCFLocalSessionTask" + guard let klass = NSClassFromString(className) else { + throw InternalError(description: "Failed to swizzle `URLSessionTask`: `\(className)` class not found.") + } + return try TaskResume(selector: self.selector, klass: klass) + } + + private init(selector: Selector, klass: AnyClass) throws { + self.method = try dd_class_getInstanceMethod(klass, selector) + super.init() + } + + func swizzle(intercept: @escaping (URLSessionTask) -> Void) { + typealias Signature = @convention(block) (URLSessionTask) -> Void + swizzle(method) { previousImplementation -> Signature in + return { task in + intercept(task) + previousImplementation(task, Self.selector) + } + } + } + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeaders.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeaders.swift new file mode 100644 index 0000000000..651c09e216 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeaders.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 + +/// W3C trace context headers as explained in +/// https://www.w3.org/TR/trace-context/#traceparent-header +public enum W3CHTTPHeaders { + /// The traceparent header represents the incoming request in a tracing system in a common format, understood by all vendors. + /// It's following a convention of `{version-format}-{trace-id}-{parent-id}-{trace-flags}`. + /// + /// Here’s an example of a traceparent header. + /// `traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01` + /// + /// **version-format** + /// + /// The following version-format definition is used for version 00. + /// + /// **trace-id** + /// + /// This is the ID of the whole trace forest and is used to uniquely identify a distributed trace through a system. + /// It is represented as a 16-byte array, for example, `4bf92f3577b34da6a3ce929d0e0e4736`. + /// All bytes as zero (`00000000000000000000000000000000`) is considered an invalid value. + /// + /// **parent-id** + /// + /// This is the ID of this request as known by the caller + /// (in some tracing systems, this is known as the span-id, where a span is the execution of a client request). + /// It is represented as an 8-byte array, for example, `00f067aa0ba902b7`. + /// All bytes as zero (`0000000000000000`) is considered an invalid value. + /// + /// **trace-flags** + /// + /// The current version of this specification only supports a single flag called sampled. + /// The sampled flag can be used to ensure that information about requests that were marked + /// for recording by the caller will also be recorded by SaaS service downstream so that the caller + /// can troubleshoot the behavior of every recorded request. + public static let traceparent = "traceparent" + + /// The main purpose of the tracestate HTTP header is to provide additional vendor-specific trace identification + /// information across different distributed tracing systems and is a companion header for the traceparent field. It + /// also conveys information about the request’s position in multiple distributed tracing graphs. + public static let tracestate = "tracestate" + + public enum Constants { + public static let version = "00" + public static let sampledValue = "01" + public static let unsampledValue = "00" + public static let separator = "-" + + // MARK: - Datadog specific tracestate keys + public static let dd = "dd" + public static let sampling = "s" + public static let origin = "o" + public static let originRUM = "rum" + public static let parentId = "p" + public static let tracestateKeyValueSeparator = ":" + public static let tracestatePairSeparator = ";" + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift new file mode 100644 index 0000000000..b8034ddaf5 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.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 + +public class W3CHTTPHeadersReader: TracePropagationHeadersReader { + private let httpHeaderFields: [String: String] + + public init(httpHeaderFields: [String: String]) { + self.httpHeaderFields = httpHeaderFields + } + + public func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { + let values = httpHeaderFields[W3CHTTPHeaders.traceparent]?.components( + separatedBy: W3CHTTPHeaders.Constants.separator + ) + + guard let traceIDValue = values?[safe: 1], + let spanIDValue = values?[safe: 2], + values?[safe: 3] != W3CHTTPHeaders.Constants.unsampledValue, + let traceID = TraceID(traceIDValue, representation: .hexadecimal), + let spanID = SpanID(spanIDValue, representation: .hexadecimal) + else { + return nil + } + + return ( + traceID: traceID, + spanID: spanID, + parentSpanID: nil + ) + } + + public var sampled: Bool? { + if let traceparent = httpHeaderFields[W3CHTTPHeaders.traceparent] { + guard let sampled = traceparent.components(separatedBy: W3CHTTPHeaders.Constants.separator).last else { + return nil + } + return sampled == W3CHTTPHeaders.Constants.sampledValue + } + + return nil + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift new file mode 100644 index 0000000000..8b88138414 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// The `W3CHTTPHeadersWriter` class facilitates the injection of trace propagation headers into network requests +/// targeted at a backend expecting [W3C propagation format](https://github.com/openzipkin/b3-propagation). +/// +/// Usage: +/// +/// var request = URLRequest(...) +/// +/// let writer = W3CHTTPHeadersWriter() +/// let span = Tracer.shared().startRootSpan(operationName: "network request") +/// Tracer.shared().inject(spanContext: span.context, writer: writer) +/// +/// writer.traceHeaderFields.forEach { (field, value) in +/// request.setValue(value, forHTTPHeaderField: field) +/// } +/// +/// // call span.finish() when the request completes +/// +public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { + /// A dictionary containing the required HTTP Headers for propagating trace information. + /// + /// Usage: + /// + /// writer.traceHeaderFields.forEach { (field, value) in + /// request.setValue(value, forHTTPHeaderField: field) + /// } + /// + public private(set) var traceHeaderFields: [String: String] = [:] + + /// A dictionary containing the tracestate to be injected. + /// This value will be merged with the tracestate from the trace context. + private let tracestate: [String: String] + + private let samplingStrategy: TraceSamplingStrategy + private let traceContextInjection: TraceContextInjection + + /// Initializes the headers writer. + /// + /// - Parameter samplingRate: The sampling rate applied for headers injection. + /// - Parameter tracestate: The tracestate to be injected. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(samplingRate: Float) { + self.init(sampleRate: samplingRate, tracestate: [:]) + } + + /// Initializes the headers writer. + /// + /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. + /// - Parameter tracestate: The tracestate to be injected. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(sampleRate: Float = 20, tracestate: [String: String] = [:]) { + self.init(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: tracestate, traceContextInjection: .all) + } + + /// Initializes the headers writer. + /// + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. + /// - Parameter tracestate: The tracestate to be injected. + /// - Parameter traceContextInjection: The strategy for injecting trace context into requests. + public init( + samplingStrategy: TraceSamplingStrategy, + tracestate: [String: String] = [:], + traceContextInjection: TraceContextInjection = .all + ) { + self.samplingStrategy = samplingStrategy + self.tracestate = tracestate + self.traceContextInjection = traceContextInjection + } + + /// Writes the trace ID, span ID, and optional parent span ID into the trace propagation headers. + /// + /// - Parameter traceID: The trace ID. + /// - Parameter spanID: The span ID. + /// - Parameter parentSpanID: The parent span ID, if applicable. + public func write(traceContext: TraceContext) { + typealias Constants = W3CHTTPHeaders.Constants + + let sampler = samplingStrategy.sampler(for: traceContext) + let sampled = sampler.sample() + + switch (traceContextInjection, sampled) { + case (.all, _), (.sampled, true): + traceHeaderFields[W3CHTTPHeaders.traceparent] = [ + Constants.version, + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue + ] + .joined(separator: Constants.separator) + + // while merging, the tracestate values from the tracestate property take precedence + // over the ones from the trace context + let tracestate: [String: String] = [ + Constants.sampling: "\(sampled ? 1 : 0)", + Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) + ].merging(tracestate) { old, new in + return new + } + + let ddtracestate = tracestate + .map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" } + .sorted() + .joined(separator: Constants.tracestatePairSeparator) + + traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)" + case (.sampled, false): + break + } + } +} diff --git a/DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift b/DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift new file mode 100644 index 0000000000..569415c7cb --- /dev/null +++ b/DatadogInternal/Sources/SDKMetrics/MethodCalledMetric.swift @@ -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 Foundation + +/// Definition of "Method Called" telemetry. +public enum MethodCalledMetric { + /// The name of this metric, included in telemetry log. + /// Note: the "[Mobile Metric]" prefix is added when sending this telemetry in RUM. + public static let name = "Method Called" + /// Metric type value. + public static let typeValue = "method called" + + /// The key for operation name. + public static let operationName = "operation_name" + /// The key for caller class. + public static let callerClass = "caller_class" + /// The key for is successful. + public static let isSuccessful = "is_successful" + /// The key for execution time. + public static let executionTime = "execution_time" +} diff --git a/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift new file mode 100644 index 0000000000..1e5bf4ff85 --- /dev/null +++ b/DatadogInternal/Sources/SDKMetrics/SDKMetricFields.swift @@ -0,0 +1,21 @@ +/* + * 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 + +/// Common fields in SDK metrics. +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. + /// + /// When attached to metric attributes, the value of this key (session ID) will be used to replace + /// the ID of session that the metric was collected in. The key itself is dropped before the metric is sent. + public static let sessionIDOverrideKey = "session_id_override" +} 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/Storage/PerformancePresetOverride.swift b/DatadogInternal/Sources/Storage/PerformancePresetOverride.swift new file mode 100644 index 0000000000..3d0e965347 --- /dev/null +++ b/DatadogInternal/Sources/Storage/PerformancePresetOverride.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 Foundation + +/// `PerformancePresetOverride` is a public structure that allows you to customize +/// performance presets by setting optional limits. If the limits are not provided, the default values from +/// the `PerformancePreset` object will be used. +public struct PerformancePresetOverride { + /// Overrides the the maximum allowed file size in bytes. + /// If not provided, the default value from the `PerformancePreset` object is used. + public let maxFileSize: UInt64? + + /// Overrides the maximum allowed object size in bytes. + /// If not provided, the default value from the `PerformancePreset` object is used. + public let maxObjectSize: UInt64? + + /// Overrides the maximum age qualifying given file for reuse (in seconds). + /// If recently used file is younger than this, it is reused - otherwise: new file is created. + public let maxFileAgeForWrite: TimeInterval? + + /// Minimum age qualifying given file for upload (in seconds). + /// If the file is older than this, it is uploaded (and then deleted if upload succeeded). + /// It has an arbitrary offset (~0.5s) over `maxFileAgeForWrite` to ensure that no upload can start for the file being currently written. + public let minFileAgeForRead: TimeInterval? + + /// Overrides the initial upload delay (in seconds). + /// At runtime, the upload interval starts with `initialUploadDelay` and then ranges from `minUploadDelay` to `maxUploadDelay` depending + /// on delivery success or failure. + public let initialUploadDelay: TimeInterval? + + /// Overrides the mininum interval of data upload (in seconds). + public let minUploadDelay: TimeInterval? + + /// Overrides the maximum interval of data upload (in seconds). + public let maxUploadDelay: TimeInterval? + + /// Overrides the current interval is change on successful upload. Should be less or equal `1.0`. + /// E.g: if rate is `0.1` then `delay` will be changed by `delay * 0.1`. + public let uploadDelayChangeRate: Double? + + /// Initializes a new `PerformancePresetOverride` instance with the provided overrides. + /// + /// - Parameters: + /// - maxFileSize: The maximum allowed file size in bytes, or `nil` to use the default value from `PerformancePreset`. + /// - maxObjectSize: The maximum allowed object size in bytes, or `nil` to use the default value from `PerformancePreset`. + /// - meanFileAge: The mean age qualifying a file for reuse, or `nil` to use the default value from `PerformancePreset`. + /// - uploadDelay: The configuration of time interval for data uploads (initial, minimum, maximum and change rate). Set `nil` to use the default value from `PerformancePreset`. + public init( + maxFileSize: UInt64?, + maxObjectSize: UInt64?, + meanFileAge: TimeInterval? = nil, + uploadDelay: (initial: TimeInterval, range: Range, changeRate: Double)? = nil + ) { + self.maxFileSize = maxFileSize + self.maxObjectSize = maxObjectSize + + if let meanFileAge = meanFileAge { + // Following constants are the same as in `DatadogCore.PerformancePreset` + self.maxFileAgeForWrite = meanFileAge * 0.95 // 5% below the mean age + self.minFileAgeForRead = meanFileAge * 1.05 // 5% above the mean age + } else { + self.maxFileAgeForWrite = nil + self.minFileAgeForRead = nil + } + + if let uploadDelay = uploadDelay { + self.initialUploadDelay = uploadDelay.initial + self.minUploadDelay = uploadDelay.range.lowerBound + self.maxUploadDelay = uploadDelay.range.upperBound + self.uploadDelayChangeRate = uploadDelay.changeRate + } else { + self.initialUploadDelay = nil + self.minUploadDelay = nil + self.maxUploadDelay = nil + self.uploadDelayChangeRate = nil + } + } +} diff --git a/DatadogInternal/Sources/Storage/Writer.swift b/DatadogInternal/Sources/Storage/Writer.swift new file mode 100644 index 0000000000..0bb8e245ad --- /dev/null +++ b/DatadogInternal/Sources/Storage/Writer.swift @@ -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 Foundation + +/// A type, writing data. +public protocol Writer { + /// Encodes given encodable value and metadata, and writes to the destination. + /// - Parameter value: Encodable value to write. + /// - Parameter metadata: Encodable metadata to write. + func write(value: T, metadata: M?) +} + +extension Writer { + /// Encodes given encodable value and writes to the destination. + /// Uses `write(value:metadata:)` with `nil` metadata. + /// - Parameter value: Encodable value to write. + public func write(value: T) { + let metadata: Data? = nil + write(value: value, metadata: metadata) + } +} diff --git a/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift b/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift new file mode 100644 index 0000000000..173c6e80e8 --- /dev/null +++ b/DatadogInternal/Sources/Swizzling/MethodSwizzler.swift @@ -0,0 +1,191 @@ +/* + * 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 + +/// Swizzling interface holds references and hierarchy of swizzled +/// methods. +internal enum Swizzling { + /// List of currently swizzled methods. + static var methods: [Method] { + sync { Array($0.keys) } + } + + /// Describes the current swizzled methods. + static var description: String { + methods.map { method_getName($0) }.description + } + + /// The hierarchy of swizzling per method. + private static var swizzlings: [Method: MethodSwizzling] = [:] + + /// lock for synchronizing `swizzlings` mutuations. + private static let lock = NSLock() + + /// Synchronization point to access the swizzling nodes. + @discardableResult + fileprivate static func sync(block: (inout [Method: MethodSwizzling]) -> T) -> T { + lock.lock() + defer { lock.unlock() } + return block(&swizzlings) + } +} + +/// Linked list of swizzled implementations. +/// +/// This object hold the previous (origin) implementation of a +/// method, the override closure reference, and a reference to its +/// parent closure. +private final class MethodSwizzling { + /// original implementation + let origin: IMP + /// type-erased override closure + let override: OverrideBox + /// parent swizzling + let parent: MethodSwizzling? + + init(origin: IMP, override: OverrideBox, parent: MethodSwizzling? = nil) { + self.origin = origin + self.override = override + self.parent = parent + } +} + +/// Reference to type-erased override closure. +private final class OverrideBox { + let closure: Any + init(_ closure: Any) { + self.closure = closure + } +} + +open class MethodSwizzler { + /// A `MethodOverride` associates an override reference to a method + /// of the Objective-C runtime. + private typealias MethodOverride = (method: Method, `override`: OverrideBox) + + /// List of overrides managed by this instance. + private var overrides: [MethodOverride] = [] + + public init() { } + + /// Swizzle a method with a closure. + /// + /// - Parameters: + /// - method: The method pointer to swizzle. + /// - override: The closure to apply. + /// + /// - Complexity: O(1) on average, over many calls to `swizzle(_:,override:)` on the + /// same array. When a swizzler needs to reallocate storage before swizzling, swizzling is O(*n*), + /// where *n* is the number of method swizzling managed by this instance. + public func swizzle(_ method: Method, override: @escaping (Signature) -> Override) { + Swizzling.sync { swizzlings in + let origin = method_override(method, override) + let override = OverrideBox(override) + + swizzlings[method] = MethodSwizzling( + origin: origin, + override: override, + parent: swizzlings[method] + ) + + overrides.append((method, override)) + } + } + + /// Removes swizzling and resets the method to its previous implementation. + /// + /// This method will remove all swizzles that have been created by the instance + /// only. Other overrides will stay in the callstack. + /// + /// - Complexity: O(*n*), where *n* is the number of the swizzle per method. + public func unswizzle() { + Swizzling.sync { swizzlings in + while let (method, override) = overrides.popLast() { + guard let swizzling = swizzlings[method] else { + continue + } + + swizzlings[method] = _unswizzle( + method: method, + override: override, + swizzling: swizzling + ) + } + } + } + + /// Unswizzle a method override. + /// + /// If found, the given override will be remove from the hierachy and swizzling will + /// be re-applied for children to also remove the override from the callstack. + /// + /// - Parameters: + /// - method: The method to unswizzle. + /// - override: The override closure to remove from swizzling. + /// - swizzling: The swizzling list. + /// - Returns: The new swizzling hierarchy if any. + private func _unswizzle(method: Method, override: OverrideBox, swizzling: MethodSwizzling) -> MethodSwizzling? { + // reset the method to its previous implementation + method_setImplementation(method, swizzling.origin) + // if override is found, stop the recursion and remove the node + // from the list by returning the parent + if swizzling.override === override { + return swizzling.parent + } + // if override is not found, go to parent (depth-first traversal) + let parent = swizzling.parent.flatMap { + _unswizzle(method: method, override: override, swizzling: $0) + } + // at this point, parents have been processed and we can re-apply + // swizzling override + guard let override = swizzling.override.closure as? (Signature) -> Override else { + // we should never get here as the closure will always + // satify the type: remove the node by returning its + // parent + return swizzling.parent + } + // re-apply swizzling for current override + return MethodSwizzling( + origin: method_override(method, override), + override: swizzling.override, + parent: parent + ) + } + + /// Overrides the implementation of a method. + /// + /// - Parameters: + /// - method: The methods to override. + /// - override: The overriding closure. + /// - Returns: The previous implementation of the method. + private func method_override(_ method: Method, _ override: @escaping (Signature) -> Override) -> IMP { + let org_imp = method_getImplementation(method) + let org = unsafeBitCast(org_imp, to: Signature.self) + let ovr: Override = override(org) + let ovr_imp: IMP = imp_implementationWithBlock(ovr) + return method_setImplementation(method, ovr_imp) + } +} + +extension MethodSwizzler: CustomDebugStringConvertible { + public var debugDescription: String { + """ + The MethodSwizzler holds swizzling for: + \(overrides.map { method_getName($0.method) }.description) + """ + } +} + +// MARK: - Find Method + +public func dd_class_getInstanceMethod(_ cls: AnyClass, _ name: Selector) throws -> Method { + guard let method = class_getInstanceMethod(cls, name) else { + throw InternalError(description: "\(NSStringFromSelector(name)) is not found in \(NSStringFromClass(cls))") + } + + return method +} diff --git a/DatadogInternal/Sources/Telemetry/CoreLogger.swift b/DatadogInternal/Sources/Telemetry/CoreLogger.swift new file mode 100644 index 0000000000..9a26c08b80 --- /dev/null +++ b/DatadogInternal/Sources/Telemetry/CoreLogger.swift @@ -0,0 +1,113 @@ +/* + * 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 + +public enum CoreLoggerLevel: Int, Comparable, CaseIterable { + /// Least severe level, meant to self-diagnose possible issues with the SDK. + /// It **should be used to log all events which might be important for us in diagnosing the SDK** + /// in user apps (e.g.: printing the SDK version or important aspects of configuration). + /// + /// No emoji prefix should be added by the logger when showing this log to the user. + case debug + + /// Level indicating **an user error when using the SDK**. It should be used for + /// logging errors that are caused by user fault (e.g. wrong configuration). + /// + /// The "⚠️" emoji prefix should be added by the logger when showing this log to the user. + case warn + + /// Level indicating **an error in the SDK**. It shuld be only used for logging errors + /// which are not caused by the user (e.g. SDK logic fault). + /// + /// The "🔥" emoji prefix should be added by the logger when showing this log to the user. + case error + + /// Most severe level for logging errors which **makes some part of the SDK unfunctional**. + /// It can be used to indicate either fatal SDK errors or user faults. + /// + /// The "⛔️" emoji prefix should be added by the logger when showing this log to the user. + case critical + + var emojiPrefix: String { + switch self { + case .debug: return "" + case .warn: return "⚠️" + case .error: return "🔥" + case .critical: return "⛔️" + } + } + + public static func < (lhs: CoreLoggerLevel, rhs: CoreLoggerLevel) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +/// The `CoreLogger` protocol defines methods to log debug information and execution errors from Datadog SDK to user console. +/// +/// It is meant for debugging purposes when using the SDK, hence **it should log information useful and actionable +/// to the SDK user**. Think of possible logs that we may want to receive from our users when asking them to enable +/// SDK verbosity and send us their console log. +public protocol CoreLogger { + /// Log the message and error using given severity level. + /// + /// - Parameters: + /// - level: the severity level + /// - message: the message to be shown + /// - error: eventual `Error` which will be showed in a nice format + func log(_ level: CoreLoggerLevel, message: @autoclosure () -> String, error: Error?) +} + +extension CoreLogger { + /// Print debug message which is meant to self-diagnose possible issues with the SDK. + /// It should be used to log all events which might be important for us in diagnosing the SDK + /// in user apps (e.g.: printing the SDK version or important aspects of configuration). + /// + /// No emoji prefix is added by the logger when priting this log to the console. + /// + /// - Parameters: + /// - message: the message + /// - error: eventual `Error` which will be printed in nice format + public func debug(_ message: @autoclosure () -> String, error: Error? = nil) { + log(.debug, message: message(), error: error) + } + + /// Print error message which indicates **an user error when using the SDK**. It should be used for + /// indicating errors that are caused by user fault (e.g. wrong configuration). + /// + /// The "⚠️" emoji prefix is added by the logger when priting this log to the console. + /// + /// - Parameters: + /// - message: the message + /// - error: eventual `Error` which will be printed in nice format + public func warn(_ message: @autoclosure () -> String, error: Error? = nil) { + log(.warn, message: message(), error: error) + } + + /// Print error message which indicates **an error in the SDK**. It shuld be only used for errors + /// which are not caused by the user (e.g. SDK user fault). + /// + /// The "🔥" emoji prefix is added by the logger when priting this log to the console. + /// + /// - Parameters: + /// - message: the message + /// - error: eventual `Error` which will be printed in nice format + public func error(_ message: @autoclosure () -> String, error: Error? = nil) { + log(.error, message: message(), error: error) + } + + /// Print error message which indicates an error which **makes some part of the SDK unfunctional**. + /// It can be used to indicate either fatal SDK errors or user fault. + /// + /// The "⛔️" emoji prefix is added by the logger when priting this log to the console. + /// + /// - Parameters: + /// - message: the message + /// - error: eventual `Error` which will be printed in nice format + public func critical(_ message: @autoclosure () -> String, error: Error? = nil) { + log(.critical, message: message(), error: error) + } +} diff --git a/DatadogInternal/Sources/Telemetry/InternalLogger.swift b/DatadogInternal/Sources/Telemetry/InternalLogger.swift new file mode 100644 index 0000000000..10d2220a8b --- /dev/null +++ b/DatadogInternal/Sources/Telemetry/InternalLogger.swift @@ -0,0 +1,75 @@ +/* + * 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 + +/// The `CoreLogger` printing to debugger console. +public struct InternalLogger: CoreLogger { + /// The prefix applied to all core logs. + private static let prefix = "[DATADOG SDK] 🐶 → " + + /// The date provider for annotating core logs. + private let dateProvider: DateProvider + /// Formatter used to format the time accordingly for local device. + private let dateFormatter: DateFormatterType + /// The print function. + private let printFunction: (String, CoreLoggerLevel) -> Void + /// V1's verbosity level. Only logs above or equal to this level wil be printed. + private let currentVerbosityLevel: () -> CoreLoggerLevel? + + public init( + dateProvider: DateProvider, + timeZone: TimeZone, + printFunction: @escaping (String, CoreLoggerLevel) -> Void, + verbosityLevel: @escaping () -> CoreLoggerLevel? + ) { + self.dateProvider = dateProvider + self.dateFormatter = presentationDateFormatter(withTimeZone: timeZone) + self.printFunction = printFunction + self.currentVerbosityLevel = verbosityLevel + } + + // MARK: - CoreLogger + + public func log(_ level: CoreLoggerLevel, message: @autoclosure () -> String, error: Error?) { + guard let verbosityLevel = currentVerbosityLevel(), level >= verbosityLevel else { + return // if no `Datadog.verbosityLevel` is set or it is set above this level + } + + print(message: message(), error: error, level: level) + } + + // MARK: - Private + + private func print(message: @autoclosure () -> String, error: Error?, level: CoreLoggerLevel) { + var log = buildMessageString(message: message(), emoji: level.emojiPrefix) + + if let error = error { + log += "\n\nError details:\n\(buildErrorString(error: error))" + } + printFunction(log, level) + } + + private func buildMessageString(message: @autoclosure () -> String, emoji: String) -> String { + let prefix = InternalLogger.prefix + let time = dateFormatter.string(from: dateProvider.now) + + if !emoji.isEmpty { + return "\(prefix)\(time) \(emoji) \(message())" + } else { + return "\(prefix)\(time) \(message())" + } + } + + private func buildErrorString(error: Error) -> String { + let dderror = DDError(error: error) + return """ + → type: \(dderror.type) + → message: \(dderror.message) + → stack: \(dderror.stack) + """ + } +} diff --git a/DatadogInternal/Sources/Telemetry/Telemetry.swift b/DatadogInternal/Sources/Telemetry/Telemetry.swift new file mode 100644 index 0000000000..1b9cb195a0 --- /dev/null +++ b/DatadogInternal/Sources/Telemetry/Telemetry.swift @@ -0,0 +1,588 @@ +/* + * 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 the type of configuration telemetry events supported by the SDK. +public struct ConfigurationTelemetry: Equatable { + public let actionNameAttribute: String? + public let allowFallbackToLocalStorage: Bool? + public let allowUntrustedEvents: Bool? + public let appHangThreshold: Int64? + public let backgroundTasksEnabled: Bool? + public let batchProcessingLevel: Int64? + public let batchSize: Int64? + public let batchUploadFrequency: Int64? + public let dartVersion: 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? + public let reactVersion: String? + public let sessionReplaySampleRate: Int64? + public let sessionSampleRate: Int64? + public let silentMultipleInit: Bool? + public let startRecordingImmediately: Bool? + public let telemetryConfigurationSampleRate: Int64? + public let telemetrySampleRate: Int64? + public let tracerAPI: String? + public let tracerAPIVersion: String? + public let traceSampleRate: Int64? + public let trackBackgroundEvents: Bool? + public let trackCrossPlatformLongTasks: Bool? + public let trackErrors: Bool? + public let trackFlutterPerformance: Bool? + public let trackFrustrations: Bool? + public let trackLongTask: Bool? + public let trackNativeErrors: Bool? + public let trackNativeLongTasks: Bool? + public let trackNativeViews: Bool? + public let trackNetworkRequests: Bool? + public let trackResources: Bool? + public let trackSessionAcrossSubdomains: Bool? + public let trackUserInteractions: Bool? + public let trackViewsManually: Bool? + public let unityVersion: String? + public let useAllowedTracingUrls: Bool? + public let useBeforeSend: Bool? + public let useExcludedActivityUrls: Bool? + public let useFirstPartyHosts: Bool? + public let useLocalEncryption: Bool? + public let useProxy: Bool? + public let useSecureSessionCookie: Bool? + public let useTracing: Bool? + 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(MetricTelemetry) + case usage(UsageTelemetry) +} + +/// The `Telemetry` protocol defines methods to collect debug information +/// and detect execution errors of the Datadog SDK. +public protocol Telemetry { + /// Sends a Telemetry message. + /// + /// - Parameter telemetry: The telemtry message + func send(telemetry: TelemetryMessage) +} + +public extension Telemetry { + /// Starts timing a method call using the "Method Called" metric. + /// + /// - Parameters: + /// - 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%. + /// + /// 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, + headSampleRate: SampleRate + ) -> MethodCalledTrace? { + if Sampler(samplingRate: headSampleRate).sample() { + return MethodCalledTrace( + operationName: operationName, + callerClass: callerClass, + headSampleRate: headSampleRate + ) + } else { + return nil + } + } + + /// 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: + /// - metric: The `MethodCalledTrace` instance. + /// - 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 { + 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 + ) + ) + ) + } + } +} + +/// A metric to measure the time of a method call. +public struct MethodCalledTrace { + let operationName: String + let callerClass: String + let headSampleRate: SampleRate + let startTime = Date() +} + +extension Telemetry { + /// Collects debug information. + /// + /// - Parameters: + /// - id: Identity of the debug log, this can be used to prevent duplicates. + /// - message: The debug message. + /// - attributes: Custom attributes attached to the log (optional). + public func debug(id: String, message: String, attributes: [String: Encodable]? = nil) { + send(telemetry: .debug(id: id, message: message, attributes: attributes)) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - id: Identity of the debug log, this can be used to prevent duplicates. + /// - message: The error message. + /// - kind: The kind of error. + /// - stack: The stack trace. + public func error(id: String, message: String, kind: String, stack: String) { + send(telemetry: .error(id: id, message: message, kind: kind, stack: stack)) + } + + /// Report a Configuration Telemetry. + /// + /// The configuration can be partial, the telemetry should support accumulation of + /// configuration for lazy initialization of the SDK. + /// + /// - Parameter configuration: The SDK configuration. + public func report(configuration: ConfigurationTelemetry) { + send(telemetry: .configuration(configuration)) + } + + /// Collects debug information. + /// + /// - Parameters: + /// - message: The debug message. + /// - attributes: Custom attributes attached to the log (optional). + /// - file: The current file name. + /// - line: The line number in file. + public func debug(_ message: String, attributes: [String: Encodable]? = nil, file: String = #fileID, line: Int = #line) { + debug(id: "\(file):\(line):\(message)", message: message, attributes: attributes) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - message: The error message. + /// - kind: The kind of error. + /// - stack: The stack trace. + /// - file: The current file name. + /// - line: The line number in file. + public func error(_ message: String, kind: String? = nil, stack: String? = nil, file: String = #fileID, line: Int = #line) { + error( + id: "\(file):\(line):\(message)", + message: message, + kind: kind ?? "\(file)", + stack: stack ?? "\(file):\(line)" + ) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - error: The error. + /// - file: The current file name. + /// - line: The line number in file. + public func error(_ error: DDError, file: String = #fileID, line: Int = #line) { + self.error(error.message, kind: error.type, stack: error.stack, file: file, line: line) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - message: The error message. + /// - error: The error. + /// - file: The current file name. + /// - line: The line number in file. + public func error(_ message: String, error: DDError, file: String = #fileID, line: Int = #line) { + self.error("\(message) - \(error.message)", kind: error.type, stack: error.stack, file: file, line: line) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - error: The error. + /// - file: The current file name. + /// - line: The line number in file. + public func error(_ error: Error, file: String = #fileID, line: Int = #line) { + self.error(DDError(error: error), file: file, line: line) + } + + /// Collect execution error. + /// + /// - Parameters: + /// - message: The error message. + /// - error: The error. + /// - file: The current file name. + /// - line: The line number in file. + public func error(_ message: String, error: Error, file: String = #fileID, line: Int = #line) { + self.error(message, error: DDError(error: error), file: file, line: line) + } + + /// Report a Configuration Telemetry. + /// + /// The configuration can be partial, the telemetry supports accumulation of + /// configuration for lazy initialization of different SDK features. + public func configuration( + actionNameAttribute: String? = nil, + allowFallbackToLocalStorage: Bool? = nil, + allowUntrustedEvents: Bool? = nil, + appHangThreshold: Int64? = nil, + backgroundTasksEnabled: Bool? = nil, + batchProcessingLevel: Int64? = nil, + batchSize: Int64? = nil, + batchUploadFrequency: Int64? = nil, + dartVersion: 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, + reactVersion: String? = nil, + sessionReplaySampleRate: Int64? = nil, + sessionSampleRate: Int64? = nil, + silentMultipleInit: Bool? = nil, + startRecordingImmediately: Bool? = nil, + telemetryConfigurationSampleRate: Int64? = nil, + telemetrySampleRate: Int64? = nil, + tracerAPI: String? = nil, + tracerAPIVersion: String? = nil, + traceSampleRate: Int64? = nil, + trackBackgroundEvents: Bool? = nil, + trackCrossPlatformLongTasks: Bool? = nil, + trackErrors: Bool? = nil, + trackFlutterPerformance: Bool? = nil, + trackFrustrations: Bool? = nil, + trackLongTask: Bool? = nil, + trackNativeErrors: Bool? = nil, + trackNativeLongTasks: Bool? = nil, + trackNativeViews: Bool? = nil, + trackNetworkRequests: Bool? = nil, + trackResources: Bool? = nil, + trackSessionAcrossSubdomains: Bool? = nil, + trackUserInteractions: Bool? = nil, + trackViewsManually: Bool? = nil, + unityVersion: String? = nil, + useAllowedTracingUrls: Bool? = nil, + useBeforeSend: Bool? = nil, + useExcludedActivityUrls: Bool? = nil, + useFirstPartyHosts: Bool? = nil, + useLocalEncryption: Bool? = nil, + useProxy: Bool? = nil, + useSecureSessionCookie: Bool? = nil, + useTracing: Bool? = nil, + useWorkerUrl: Bool? = nil + ) { + self.report(configuration: .init( + actionNameAttribute: actionNameAttribute, + allowFallbackToLocalStorage: allowFallbackToLocalStorage, + allowUntrustedEvents: allowUntrustedEvents, + appHangThreshold: appHangThreshold, + backgroundTasksEnabled: backgroundTasksEnabled, + batchProcessingLevel: batchProcessingLevel, + batchSize: batchSize, + batchUploadFrequency: batchUploadFrequency, + dartVersion: dartVersion, + forwardErrorsToLogs: forwardErrorsToLogs, + defaultPrivacyLevel: defaultPrivacyLevel, + textAndInputPrivacyLevel: textAndInputPrivacyLevel, + imagePrivacyLevel: imagePrivacyLevel, + touchPrivacyLevel: touchPrivacyLevel, + initializationType: initializationType, + mobileVitalsUpdatePeriod: mobileVitalsUpdatePeriod, + reactNativeVersion: reactNativeVersion, + reactVersion: reactVersion, + sessionReplaySampleRate: sessionReplaySampleRate, + sessionSampleRate: sessionSampleRate, + silentMultipleInit: silentMultipleInit, + startRecordingImmediately: startRecordingImmediately, + telemetryConfigurationSampleRate: telemetryConfigurationSampleRate, + telemetrySampleRate: telemetrySampleRate, + tracerAPI: tracerAPI, + tracerAPIVersion: tracerAPIVersion, + traceSampleRate: traceSampleRate, + trackBackgroundEvents: trackBackgroundEvents, + trackCrossPlatformLongTasks: trackCrossPlatformLongTasks, + trackErrors: trackErrors, + trackFlutterPerformance: trackFlutterPerformance, + trackFrustrations: trackFrustrations, + trackLongTask: trackLongTask, + trackNativeErrors: trackNativeErrors, + trackNativeLongTasks: trackNativeLongTasks, + trackNativeViews: trackNativeViews, + trackNetworkRequests: trackNetworkRequests, + trackResources: trackResources, + trackSessionAcrossSubdomains: trackSessionAcrossSubdomains, + trackUserInteractions: trackUserInteractions, + trackViewsManually: trackViewsManually, + unityVersion: unityVersion, + useAllowedTracingUrls: useAllowedTracingUrls, + useBeforeSend: useBeforeSend, + useExcludedActivityUrls: useExcludedActivityUrls, + useFirstPartyHosts: useFirstPartyHosts, + useLocalEncryption: useLocalEncryption, + useProxy: useProxy, + useSecureSessionCookie: useSecureSessionCookie, + useTracing: useTracing, + useWorkerUrl: useWorkerUrl + )) + } + + /// Collects a metric value. + /// + /// 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 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))) + } +} + +public struct NOPTelemetry: Telemetry { + public init() { } + /// no-op + public func send(telemetry: TelemetryMessage) { } + public func startMethodCalled(operationName: String, callerClass: String, samplingRate: Float) -> MethodCalledTrace? { return nil } + public func stopMethodCalled(_ metric: MethodCalledTrace?, isSuccessful: Bool) { } +} + +internal struct CoreTelemetry: Telemetry { + /// A weak core reference. + private weak var core: DatadogCoreProtocol? + + /// Creates a Telemetry associated with a core instance. + /// + /// The `CoreTelemetry` keeps a weak reference + /// to the provided core. + /// + /// - Parameter core: The core instance. + init(core: DatadogCoreProtocol) { + self.core = core + } + + /// Sends a Telemetry message. + /// + /// The Telemetry message will be transmitted on the message-bus + /// of the core. + /// + /// - Parameter telemetry: The telemtry message. + func send(telemetry: TelemetryMessage) { + core?.send(message: .telemetry(telemetry)) + } +} + +extension DatadogCoreProtocol { + /// Telemetry endpoint. + /// + /// Use this property to report any telemetry event to the core. + 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( + actionNameAttribute: other.actionNameAttribute ?? actionNameAttribute, + allowFallbackToLocalStorage: other.allowFallbackToLocalStorage ?? allowFallbackToLocalStorage, + allowUntrustedEvents: other.allowUntrustedEvents ?? allowUntrustedEvents, + appHangThreshold: other.appHangThreshold ?? appHangThreshold, + backgroundTasksEnabled: other.backgroundTasksEnabled ?? backgroundTasksEnabled, + batchProcessingLevel: other.batchProcessingLevel ?? batchProcessingLevel, + batchSize: other.batchSize ?? batchSize, + batchUploadFrequency: other.batchUploadFrequency ?? batchUploadFrequency, + dartVersion: other.dartVersion ?? dartVersion, + 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, + reactVersion: other.reactVersion ?? reactVersion, + sessionReplaySampleRate: other.sessionReplaySampleRate ?? sessionReplaySampleRate, + sessionSampleRate: other.sessionSampleRate ?? sessionSampleRate, + silentMultipleInit: other.silentMultipleInit ?? silentMultipleInit, + startRecordingImmediately: other.startRecordingImmediately ?? startRecordingImmediately, + telemetryConfigurationSampleRate: other.telemetryConfigurationSampleRate ?? telemetryConfigurationSampleRate, + telemetrySampleRate: other.telemetrySampleRate ?? telemetrySampleRate, + tracerAPI: other.tracerAPI ?? tracerAPI, + tracerAPIVersion: other.tracerAPIVersion ?? tracerAPIVersion, + traceSampleRate: other.traceSampleRate ?? traceSampleRate, + trackBackgroundEvents: other.trackBackgroundEvents ?? trackBackgroundEvents, + trackCrossPlatformLongTasks: other.trackCrossPlatformLongTasks ?? trackCrossPlatformLongTasks, + trackErrors: other.trackErrors ?? trackErrors, + trackFlutterPerformance: other.trackFlutterPerformance ?? trackFlutterPerformance, + trackFrustrations: other.trackFrustrations ?? trackFrustrations, + trackLongTask: other.trackLongTask ?? trackLongTask, + trackNativeErrors: other.trackNativeErrors ?? trackNativeErrors, + trackNativeLongTasks: other.trackNativeLongTasks ?? trackNativeLongTasks, + trackNativeViews: other.trackNativeViews ?? trackNativeViews, + trackNetworkRequests: other.trackNetworkRequests ?? trackNetworkRequests, + trackResources: other.trackResources ?? trackResources, + trackSessionAcrossSubdomains: other.trackSessionAcrossSubdomains ?? trackSessionAcrossSubdomains, + trackUserInteractions: other.trackUserInteractions ?? trackUserInteractions, + trackViewsManually: other.trackViewsManually ?? trackViewsManually, + unityVersion: other.unityVersion ?? unityVersion, + useAllowedTracingUrls: other.useAllowedTracingUrls ?? useAllowedTracingUrls, + useBeforeSend: other.useBeforeSend ?? useBeforeSend, + useExcludedActivityUrls: other.useExcludedActivityUrls ?? useExcludedActivityUrls, + useFirstPartyHosts: other.useFirstPartyHosts ?? useFirstPartyHosts, + useLocalEncryption: other.useLocalEncryption ?? useLocalEncryption, + useProxy: other.useProxy ?? useProxy, + useSecureSessionCookie: other.useSecureSessionCookie ?? useSecureSessionCookie, + useTracing: other.useTracing ?? useTracing, + useWorkerUrl: other.useWorkerUrl ?? useWorkerUrl + ) + } +} diff --git a/DatadogInternal/Sources/Upload/DataCompression.swift b/DatadogInternal/Sources/Upload/DataCompression.swift new file mode 100644 index 0000000000..b43a830700 --- /dev/null +++ b/DatadogInternal/Sources/Upload/DataCompression.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import Compression +import zlib + +/// `Deflate` provides static methods to deflate `Data` for HTTP `deflate` `Content-Encoding` +/// using the `zlib` structure defined in IETF RFC 1950 with the deflate compression algorithm defined in +/// IETF RFC 1951. +/// +/// ref: +/// - https://zlib.net/ +/// - https://datatracker.ietf.org/doc/html/rfc1950 +/// - https://datatracker.ietf.org/doc/html/rfc1951 +internal struct Deflate { + /// Compresses the data into `ZLIB` data format. + /// + /// The `Compression` library implements the zlib encoder at level 5 only. This compression level + /// provides a good balance between compression speed and compression ratio. + /// + /// The encoded format is the ZLIB Compressed Data Format as described in IETF RFC 1950 + /// https://datatracker.ietf.org/doc/html/rfc1950 + /// + /// - Parameter data: Source data to deflate + /// - Returns: The compressed data format. + static func encode(_ data: Data) -> Data? { + // 2 bytes header - defines the compression mode + // + // +---+---+ + // |CMF|FLG| + // +---+---+ + // + // ref. https://datatracker.ietf.org/doc/html/rfc1950#section-2.2 + // + // The following header value is from `mw99/DataCompression` which applies + // the same compression algorithm defined by `COMPRESSION_ZLIB` + // ref. https://github.com/mw99/DataCompression + let header = Data([0x78, 0x5e]) + + guard + let raw = compress(data), + let checksum = adler32(data), + // Returns `nil` when compression expands the data size. + data.count > header.count + raw.count + checksum.count + else { return nil } + + return header + raw + checksum + } + + /// Compresses the data using the `ZLIB` compression algorithm. + /// + /// The `Compression` library implements the zlib encoder at level 5 only. This compression level + /// provides a good balance between compression speed and compression ratio. + /// + /// The encoded format is the raw DEFLATE format as described in in IETF RFC 1951 + /// https://datatracker.ietf.org/doc/html/rfc1951 + /// + /// This deflate implementation uses `compression_encode_buffer(_:_:_:_:_:_:)` + /// from the `Compression` framework by allocating a destination buffer of source size and copying + /// the result into a `Data` structure. In the worst possible case, where the compression expands the + /// data size, the destination buffer becomes too small and deflation returns `nil`. + /// + /// ref. https://developer.apple.com/documentation/compression/1480986-compression_encode_buffer + /// + /// - Parameter data: Source data to deflate + /// - Returns: The compressed data. If the compressed data size is bigger than the source size, + /// or an error occurs, `nil` is returned. + static func compress(_ data: Data) -> Data? { + return data.withUnsafeBytes { + guard let ptr = $0.bindMemory(to: UInt8.self).baseAddress else { + return nil + } + + let buffer = UnsafeMutablePointer.allocate(capacity: data.count) + defer { buffer.deallocate() } + + // The number of bytes written to the destination buffer after compressing + // the input. If the funtion can't compress the entire input to fit into + // the provided destination buffer, or an error occurs, 0 is returned. + let size = compression_encode_buffer(buffer, data.count, ptr, data.count, nil, COMPRESSION_ZLIB) + guard size > 0 else { + return nil + } + + return Data(bytes: buffer, count: size) + } + } + + /// Calculates the Adler32 checksum of the given data. + /// + /// An Adler-32 checksum is almost as reliable as a CRC-32 but can be computed much faster. + /// + /// - Parameter data: Data to compute the checksum. + /// - Returns: The Adler-32 checksum. + static func adler32(_ data: Data) -> Data? { + let adler: uLong? = data.withUnsafeBytes { + guard let ptr = $0.bindMemory(to: Bytef.self).baseAddress else { + return nil + } + + // The Adler-32 checksum should be initialized to 1 as described in + // https://datatracker.ietf.org/doc/html/rfc1950#section-8 + return zlib.adler32(1, ptr, uInt(data.count)) + } + + guard let checksum = adler else { + return nil + } + + var bytes = UInt32(checksum).bigEndian + return Data(bytes: &bytes, count: MemoryLayout.size) + } +} diff --git a/DatadogInternal/Sources/Upload/DataFormat.swift b/DatadogInternal/Sources/Upload/DataFormat.swift new file mode 100644 index 0000000000..a1674946b9 --- /dev/null +++ b/DatadogInternal/Sources/Upload/DataFormat.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 + +/// Describes the format of writing and reading data from files. +public struct DataFormat { + /// Prefixes the batch payload read from file. + private let prefixData: Data + /// Suffixes the batch payload read from file. + private let suffixData: Data + /// Separates entities written to file. + private let separatorByte: UInt8 + + // MARK: - Initialization + + public init( + prefix: String, + suffix: String, + separator: Character + ) { + self.prefixData = prefix.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + self.suffixData = suffix.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + self.separatorByte = separator.asciiValue! // swiftlint:disable:this force_unwrapping + } + + /// Formats the given data sequence by applying the prefix, separator, + /// and suffix. + /// + /// - Parameter data: The data sequence. + /// - Returns: the formatted data. + public func format(_ data: [Data]) -> Data { + // add prefix + prefixData + + // concat data + data.reduce(.init()) { $0 + $1 + [separatorByte] } + // drop last separator + .dropLast() + + // add suffix + suffixData + } +} diff --git a/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift b/DatadogInternal/Sources/Upload/DefaultJSONEncoder.swift new file mode 100644 index 0000000000..e6fc899d85 --- /dev/null +++ b/DatadogInternal/Sources/Upload/DefaultJSONEncoder.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 + +extension JSONEncoder: DatadogExtended { } + +extension DatadogExtension where ExtendedType == JSONEncoder { + public static func `default`() -> JSONEncoder { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .custom { date, encoder in + var container = encoder.singleValueContainer() + let formatted = iso8601DateFormatter.string(from: date) + try container.encode(formatted) + } + if #available(iOS 13, tvOS 13, *) { + encoder.outputFormatting = [.withoutEscapingSlashes] + } + return encoder + } +} diff --git a/DatadogInternal/Sources/Upload/Event.swift b/DatadogInternal/Sources/Upload/Event.swift new file mode 100644 index 0000000000..f2ff635f0b --- /dev/null +++ b/DatadogInternal/Sources/Upload/Event.swift @@ -0,0 +1,30 @@ +/* + * 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 + +/// Struct representing a single event. +public struct Event: Equatable { + /// Data representing the event. + public let data: Data + + /// Metadata associated with the event. + /// Metadata is optional and may be `nil` but of very small size. + /// This allows us to skip resource intensive operations in case such + /// as filtering of the events. + public let metadata: Data? + + public init(data: Data, metadata: Data? = nil) { + self.data = data + self.metadata = metadata + } +} + +extension Event: CustomDebugStringConvertible { + public var debugDescription: String { + return .init(data: data, encoding: .utf8) ?? "" + } +} diff --git a/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.swift new file mode 100644 index 0000000000..c184ec3d31 --- /dev/null +++ b/DatadogInternal/Sources/Upload/FeatureRequestBuilder.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 Foundation + +/// The `FeatureRequestBuilder` defines an interface for building a single `URLRequest` +/// for a list of data events and the current core context. +/// +/// A Feature should use this interface for creating requests that needs be sent to its Datadog Intake. +/// The request will be transported by `DatadogCore`. +public protocol FeatureRequestBuilder { + /// Builds an `URLRequest` for a list of events and the current core context to be uploaded + /// to the Feature's Intake. + /// + /// The returned request must include all necessary information, i.e. HTTP headers and + /// URL queries required by the Feature's Intake. The request will be sent by the core. + /// + /// **Note:** When `Error` is thrown, underlying data will be dropped permanently and never retried. The + /// implementation should make a wise consideration of throwing vs recovering strategy. + /// + /// - Parameters: + /// - context: The current core context. + /// - events: The events data to be uploaded. + /// - Returns: The URL request. + 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 new file mode 100644 index 0000000000..8104417bc0 --- /dev/null +++ b/DatadogInternal/Sources/Upload/URLRequestBuilder.swift @@ -0,0 +1,172 @@ +/* + * 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 + +/// Builds `URLRequest` for sending data to Datadog. +public struct URLRequestBuilder { + public enum QueryItem { + /// `ddsource={source}` query item + case ddsource(source: String) + /// `ddtags={tag1},{tag2},...` query item + case ddtags(tags: [String]) + } + + public struct HTTPHeader { + public static let contentTypeHeaderField = "Content-Type" + public static let contentEncodingHeaderField = "Content-Encoding" + public static let userAgentHeaderField = "User-Agent" + public static let ddAPIKeyHeaderField = "DD-API-KEY" + 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 + case textPlainUTF8 + case multipartFormData(boundary: String) + + public var toString: String { + switch self { + case .applicationJSON: return "application/json" + case .textPlainUTF8: return "text/plain;charset=UTF-8" + case .multipartFormData(let boundary): return "multipart/form-data; boundary=\(boundary)" + } + } + } + + let field: String + let value: () -> String + + public init(field: String, value: @escaping () -> String) { + self.field = field + self.value = value + } + + // MARK: - Standard Headers + + /// Standard "Content-Type" header. + public static func contentTypeHeader(contentType: ContentType) -> HTTPHeader { + return HTTPHeader(field: contentTypeHeaderField, value: { contentType.toString }) + } + + /// Standard "User-Agent" header. + public static func userAgentHeader(appName: String, appVersion: String, device: DeviceInfo) -> HTTPHeader { + var sanitizedAppName = appName + + if let regex = try? NSRegularExpression(pattern: "[^a-zA-Z0-9 -]+") { + sanitizedAppName = regex.stringByReplacingMatches( + in: appName, + range: NSRange(appName.startIndex.. HTTPHeader { + return HTTPHeader(field: ddAPIKeyHeaderField, value: { clientToken }) + } + + /// An observability and troubleshooting Datadog header for tracking the origin which is sending the request. + public static func ddEVPOriginHeader(source: String) -> HTTPHeader { + return HTTPHeader(field: ddEVPOriginHeaderField, value: { source }) + } + + /// An observability and troubleshooting Datadog header for tracking the origin which is sending the request. + public static func ddEVPOriginVersionHeader(sdkVersion: String) -> HTTPHeader { + return HTTPHeader(field: ddEVPOriginVersionHeaderField, value: { sdkVersion }) + } + + /// An optional Datadog header for debugging Intake requests by their ID. + 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 + /// HTTP headers. + private let headers: [HTTPHeader] + /// Telemetry interface. + private let telemetry: Telemetry + + // MARK: - Initialization + + public init( + url: URL, + queryItems: [QueryItem], + headers: [HTTPHeader], + telemetry: Telemetry = NOPTelemetry() + ) { + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if !queryItems.isEmpty { + urlComponents?.queryItems = queryItems.map { .init($0) } + } + + self.url = urlComponents?.url ?? url + self.headers = headers + self.telemetry = telemetry + } + + /// Creates `URLRequest` for uploading given `body` to Datadog. + /// + /// - Parameter body: HTTP body to be attached to request + /// - Parameter compress: if `body` should be compressed into ZLIB Compressed Data Format (IETF RFC 1950) + /// - Returns: the `URLRequest` object. + public func uploadRequest(with body: Data, compress: Bool = true) -> URLRequest { + var request = URLRequest(url: url) + var headers: [String: String] = [:] + self.headers.forEach { headers[$0.field] = $0.value() } + request.httpMethod = "POST" + + if compress, let deflatedBody = Deflate.encode(body) { + headers[HTTPHeader.contentEncodingHeaderField] = "deflate" + request.httpBody = deflatedBody + } else { + request.httpBody = body + if compress { + telemetry.debug( + """ + Failed to compress request payload + - url: \(url) + - uncompressed-size: \(body.count) + """ + ) + } + } + + headers.forEach { field, value in + request.setValue(value, forHTTPHeaderField: field) + } + return request + } +} + +extension URLQueryItem { + init(_ query: URLRequestBuilder.QueryItem) { + switch query { + case .ddsource(let source): + self = URLQueryItem(name: "ddsource", value: source) + case .ddtags(let tags): + self = URLQueryItem(name: "ddtags", value: tags.joined(separator: ",")) + } + } +} diff --git a/DatadogInternal/Sources/Upload/UploadPerformancePreset.swift b/DatadogInternal/Sources/Upload/UploadPerformancePreset.swift new file mode 100644 index 0000000000..dcd0e62bbb --- /dev/null +++ b/DatadogInternal/Sources/Upload/UploadPerformancePreset.swift @@ -0,0 +1,21 @@ +/* + * 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 + +public protocol UploadPerformancePreset { + /// Initial upload delay (in seconds). + /// At runtime, the upload interval starts with `initialUploadDelay` and then ranges from `minUploadDelay` to `maxUploadDelay` depending + /// on delivery success or failure. + var initialUploadDelay: TimeInterval { get } + /// Mininum interval of data upload (in seconds). + var minUploadDelay: TimeInterval { get } + /// Maximum interval of data upload (in seconds). + var maxUploadDelay: TimeInterval { get } + /// If upload succeeds or fails, current interval is changed by this rate. Should be less or equal `1.0`. + /// E.g: if rate is `0.1` then `delay` can be increased or decreased by `delay * 0.1`. + var uploadDelayChangeRate: Double { get } +} diff --git a/DatadogInternal/Sources/Utils/DDError.swift b/DatadogInternal/Sources/Utils/DDError.swift new file mode 100644 index 0000000000..ca40d8a63f --- /dev/null +++ b/DatadogInternal/Sources/Utils/DDError.swift @@ -0,0 +1,141 @@ +/* + * 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 + +/// Common representation of Swift `Error` used by different features. +public struct DDError: Equatable, Codable, PassthroughAnyCodable { + /// Common error key encoding threads information in Crash Reporting. + /// See "RFC - iOS Crash Reports Minimization" for more context. + public static let threads = "error.threads" + /// Common error key encoding binary images information in Crash Reporting. + /// See "RFC - iOS Crash Reports Minimization" for more context. + public static let binaryImages = "error.binary_images" + /// Common error key encoding crash meta information in Crash Reporting. + /// See "RFC - iOS Crash Reports Minimization" for more context. + public static let meta = "error.meta" + /// Common error key encoding boolean flag - `true` if any stack trace was truncated, otherwise `false`. + /// See "RFC - iOS Crash Reports Minimization" for more context. + public static let wasTruncated = "error.was_truncated" + + public let type: String + public let message: String + public let stack: String + public let sourceType: String + + public init(type: String, message: String, stack: String, sourceType: String = "ios") { + self.type = type + self.message = message + self.stack = stack + self.sourceType = sourceType + } +} + +extension DDError { + public init(error: Error) { + if isNSErrorOrItsSubclass(error) { + let nsError = error as NSError + self.type = "\(nsError.domain) - \(nsError.code)" + if nsError.userInfo[NSLocalizedDescriptionKey] != nil { + self.message = nsError.localizedDescription + } else { + self.message = nsError.description + } + self.stack = "\(nsError)" + } else { + let swiftError = error + self.type = "\(Swift.type(of: swiftError))" + self.message = "\(swiftError)" + self.stack = "\(swiftError)" + } + + self.sourceType = "ios" + } +} + +private func isNSErrorOrItsSubclass(_ error: Error) -> Bool { + var mirror: Mirror? = Mirror(reflecting: error) + + while mirror != nil { + if mirror?.subjectType == NSError.self { + return true + } + mirror = mirror?.superclassMirror + } + return false +} + +/// An exception thrown due to programmer error when calling SDK public API. +/// It makes the SDK non-functional and print the error to developer in debugger console.. +/// When thrown, check if configuration passed to `Datadog.initialize(...)` is correct +/// and if you do not call any other SDK methods before it returns. +public struct ProgrammerError: Error, CustomStringConvertible { + public let description: String + public init(description: String) { + self.description = "🔥 Datadog SDK usage error: \(description)" + } +} + +/// An exception thrown internally by SDK. +/// It is always handled by SDK (keeps it functional) and never passed to the user until `Datadog.verbosity` is set (then it might be printed in debugger console). +/// `InternalError` might be thrown due to programmer error (API misuse) or SDK internal inconsistency or external issues (e.g. I/O errors). The SDK +/// should always recover from that failures. +public struct InternalError: Error, CustomStringConvertible { + public let description: String + + public init(description: String) { + 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 new file mode 100644 index 0000000000..ec32ed44d5 --- /dev/null +++ b/DatadogInternal/Sources/Utils/DateFormatting.swift @@ -0,0 +1,32 @@ +/* + * 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 + +public protocol DateFormatterType: Sendable { + func string(from date: Date) -> String + func date(from string: String) -> Date? +} + +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 = { + 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). +public func presentationDateFormatter(withTimeZone timeZone: TimeZone = .current) -> DateFormatterType { + let formatter = DateFormatter() + formatter.timeZone = timeZone + formatter.calendar = Calendar(identifier: .gregorian) + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter +} diff --git a/DatadogInternal/Sources/Utils/Sampler.swift b/DatadogInternal/Sources/Utils/Sampler.swift new file mode 100644 index 0000000000..4e7c2e6e4d --- /dev/null +++ b/DatadogInternal/Sources/Utils/Sampler.swift @@ -0,0 +1,78 @@ +/* + * 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 + +/// 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. + /// + /// - Returns: A boolean value indicating whether sampling should occur. + /// `true` if the sample should be kept, `false` if it should be dropped. + func sample() -> Bool +} + +/// 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: SampleRate + + public init(samplingRate: SampleRate) { + self.samplingRate = max(0, min(100, samplingRate)) + } + + /// Based on the sampling rate, it returns random value deciding if an event should be "sampled" or not. + /// - Returns: `true` if event should be sent to Datadog and `false` if it should be dropped. + public func sample() -> Bool { + return Float.random(in: 0.0..<100.0) < samplingRate + } +} + +/// 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: SampleRate + /// Persisted sampling decision. + private let shouldSample: Bool + + 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/SwiftExtensions.swift b/DatadogInternal/Sources/Utils/SwiftExtensions.swift new file mode 100644 index 0000000000..5d661ba5c8 --- /dev/null +++ b/DatadogInternal/Sources/Utils/SwiftExtensions.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +// MARK: - Optional + +extension Optional { + public func ifNotNil(_ closure: (Wrapped) throws -> Void) rethrows { + if case .some(let unwrappedValue) = self { + try closure(unwrappedValue) + } + } +} + +extension Double { + public func divideIfNotZero(by divider: Self) -> Self? { + if divider == 0 { + return nil + } + return self / divider + } + + public var inverted: Self { + return self == 0 ? 0 : 1 / self + } +} + +// MARK: - UUID + +extension UUID { + /// An UUID with all zeroes (`00000000-0000-0000-0000-000000000000`). + /// Used to represent "null" in types that cannot be given a proper UUID (e.g. rejected RUM session). + public static let nullUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000") ?? UUID() +} + +// MARK: - TimeInterval + +extension TimeInterval { + public init(fromMilliseconds milliseconds: Int64) { + self = Double(milliseconds) / 1_000 + } + + /// `TimeInterval` represented in milliseconds (capped to `.min` or `.max` respectively to its sign). + public var toMilliseconds: UInt64 { + let milliseconds = self * 1_000 + return UInt64(withNoOverflow: milliseconds) + } + + /// `TimeInterval` represented in milliseconds (capped to `.min` or `.max` respectively to its sign). + public var toInt64Milliseconds: Int64 { + let miliseconds = self * 1_000 + return Int64(withNoOverflow: miliseconds) + } + + /// `TimeInterval` represented in nanoseconds (capped to `.min` or `.max` respectively to its sign). + /// Note: as `TimeInterval` yields sub-millisecond precision the nanoseconds precission will be lost. + public var toNanoseconds: UInt64 { + let nanoseconds = self * 1_000_000_000 + return UInt64(withNoOverflow: nanoseconds) + } + + /// `TimeInterval` represented in nanoseconds (capped to `.min` or `.max` respectively to its sign). + /// Note: as `TimeInterval` yields sub-millisecond precision the nanoseconds precission will be lost. + public var toInt64Nanoseconds: Int64 { + let nanoseconds = self * 1_000_000_000 + return Int64(withNoOverflow: nanoseconds) + } +} + +// MARK: - Safe floating point to integer conversion + +public enum FixedWidthIntegerError: Error { + case overflow(overflowingValue: T) +} + +extension FixedWidthInteger { + /* NOTE: RUMM-182 + Self(:) is commonly used for conversion, however it fatalError() in case of conversion failure + Self(exactly:) does the exact same thing internally yet it returns nil instead of fatalError() + It is not trivial to guess if the conversion would fail or succeed, therefore we use Self(exactly:) + so that we don't need to guess in order to save the app from crashing + + IMPORTANT: If you pass floatingPoint to Self(exactly:) without rounded(), it may return nil + */ + public init(withReportingOverflow floatingPoint: T) throws { + guard let converted = Self(exactly: floatingPoint.rounded()) else { + throw FixedWidthIntegerError.overflow(overflowingValue: floatingPoint) + } + self = converted + } + + /// Converts floating point value to fixed width integer with preventing overflow (and crash). + /// In case of overflow, the value is converted to `.min` or `.max` respectively to its sign. + /// - Parameter floatingPoint: the value to convert + public init(withNoOverflow floatingPoint: T) { + if let converted = Self(exactly: floatingPoint.rounded()) { + self = converted + } else { // overflow occurred + switch floatingPoint.sign { + case .minus: self = .min + case .plus: self = .max + } + } + } +} + +// MARK: - Array + +extension Array { + public subscript (safe index: Index) -> Element? { + 0 <= index && index < count ? self[index] : nil + } +} diff --git a/DatadogInternal/Sources/Utils/UIKitExtensions.swift b/DatadogInternal/Sources/Utils/UIKitExtensions.swift new file mode 100644 index 0000000000..c9984f3dc6 --- /dev/null +++ b/DatadogInternal/Sources/Utils/UIKitExtensions.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-2020 Datadog, Inc. + */ + +import Foundation + +#if canImport(UIKit) && !os(watchOS) +import UIKit + +extension DatadogExtension where ExtendedType == UIApplication { + /// `UIApplication.shared` does not compile in some environments (e.g. notification service app extension), resulting with: + /// _"shared' is unavailable in application extensions for iOS: Use view controller based solutions where appropriate instead"_. + /// + /// As a workaround, this `managedShared` utility provides a key-path access to the `UIApplication.shared` to make the compiler pass. + public static var managedShared: UIApplication? { + return UIApplication + .value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication // swiftlint:disable:this unsafe_uiapplication_shared + } +} + +extension UIApplication: DatadogExtended { } +#endif diff --git a/DatadogInternal/Sources/Utils/WatchKitExtensions.swift b/DatadogInternal/Sources/Utils/WatchKitExtensions.swift new file mode 100644 index 0000000000..8b08c2267d --- /dev/null +++ b/DatadogInternal/Sources/Utils/WatchKitExtensions.swift @@ -0,0 +1,19 @@ +/* + * 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(WatchKit) +import WatchKit + +extension DatadogExtension where ExtendedType == WKExtension { + public static var shared: WKExtension { + .shared() + } +} + +extension WKExtension: DatadogExtended { } +#endif diff --git a/DatadogInternal/Tests/Codable/AnyCodableTests.swift b/DatadogInternal/Tests/Codable/AnyCodableTests.swift new file mode 100644 index 0000000000..964f4bae91 --- /dev/null +++ b/DatadogInternal/Tests/Codable/AnyCodableTests.swift @@ -0,0 +1,303 @@ +/* + * 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. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +import XCTest +import TestUtilities + +@testable import DatadogInternal + +class AnyCodableTests: XCTestCase { + struct SomeCodable: Codable { + var string: String + var int: Int + var bool: Bool + var hasUnderscore: String + + enum CodingKeys: String,CodingKey { + case string + case int + case bool + case hasUnderscore = "has_underscore" + } + } + + /// Sample struct used to test complex `Encodable` types encoding + private struct Foo: Encodable, RandomMockable { + var bar: String + var bizz: Bizz + + struct Bizz: Encodable { + var buzz: String + var bazz: [Int: Int] + } + + static func mockRandom() -> Foo { + return Foo( + bar: .mockRandom(), + bizz: .init( + buzz: .mockRandom(), + bazz: [1: 2, 3: 4] + ) + ) + } + } + + func testJSONDecoding() throws { + let json = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "null": null + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let dictionary = try decoder.decode([String: AnyCodable].self, from: json) + + XCTAssertEqual(dictionary["boolean"]?.value as! Bool, true) + XCTAssertEqual(dictionary["integer"]?.value as! Int, 42) + XCTAssertEqual(dictionary["double"]?.value as! Double, 3.141592653589793, accuracy: 0.001) + XCTAssertEqual(dictionary["string"]?.value as! String, "string") + XCTAssertEqual(dictionary["array"]?.value as! [Int], [1, 2, 3]) + XCTAssertEqual(dictionary["nested"]?.value as! [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) + XCTAssertEqual(dictionary["null"]?.value as! NSNull, NSNull()) + } + + func testJSONDecodingEquatable() throws { + let json = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "null": null + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let dictionary1 = try decoder.decode([String: AnyCodable].self, from: json) + let dictionary2 = try decoder.decode([String: AnyCodable].self, from: json) + + XCTAssertEqual(dictionary1["boolean"], dictionary2["boolean"]) + XCTAssertEqual(dictionary1["integer"], dictionary2["integer"]) + XCTAssertEqual(dictionary1["double"], dictionary2["double"]) + XCTAssertEqual(dictionary1["string"], dictionary2["string"]) + XCTAssertEqual(dictionary1["array"], dictionary2["array"]) + XCTAssertEqual(dictionary1["nested"], dictionary2["nested"]) + XCTAssertEqual(dictionary1["null"], dictionary2["null"]) + } + + func testJSONEncoding() throws { + let someCodable = AnyCodable( + SomeCodable( + string: "String", + int: 100, + bool: true, + hasUnderscore: "another string" + ) + ) + + let injectedValue = 1_234 + let dictionary: [String: Any?] = [ + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "stringInterpolation": "string \(injectedValue)", + "array": [1, 2, 3], + "nested": [ + "a": "alpha", + "b": "bravo", + "c": "charlie", + ], + "someCodable": someCodable, + "null": nil + ] + + let encoder = JSONEncoder() + + let json = try encoder.encode(AnyEncodable(dictionary)) + let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary + + let expected = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "stringInterpolation": "string 1234", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "someCodable": { + "string":"String", + "int":100, + "bool": true, + "has_underscore":"another string" + }, + "null": null + } + """.data(using: .utf8)! + let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary + + XCTAssertEqual(encodedJSONObject, expectedJSONObject) + } + + func testGivenEncodableValueWrappedIntoCodableValue_whenEncoding_itProducesExpectedJSONRepresentation() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + + func json(for value: T) throws -> String { + // Given + let codableValue = AnyEncodable(value) + + // When + let encodedCodableValue = try encoder.encode(codableValue) + + // Then + return encodedCodableValue.utf8String + } + + if #available(iOS 13.0, *) { + XCTAssertEqual(try json(for: true), "true") + XCTAssertEqual(try json(for: false), "false") + XCTAssertEqual(try json(for: 123), "123") + XCTAssertEqual(try json(for: -123), "-123") + XCTAssertEqual(try json(for: 123.45), "123.45") + XCTAssertEqual(try json(for: "string"), "\"string\"") + + let url = URL(string: "https://example.com/image.png")! + XCTAssertEqual(try json(for: url), #""https:\/\/example.com\/image.png""#) + + // swiftlint:disable syntactic_sugar + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + XCTAssertEqual(try json(for: Optional.none), "null") + // swiftlint:enable syntactic_sugar + } else { + XCTAssertEqual(try json(for: EncodingContainer(true)), #"{"value":true}"#) + XCTAssertEqual(try json(for: EncodingContainer(false)), #"{"value":false}"#) + XCTAssertEqual(try json(for: EncodingContainer(123)), #"{"value":123}"#) + XCTAssertEqual(try json(for: EncodingContainer(-123)), #"{"value":-123}"#) + XCTAssertEqual(try json(for: EncodingContainer(123.45)), #"{"value":123.45}"#) + XCTAssertEqual(try json(for: EncodingContainer("string")), #"{"value":"string"}"#) + + let url = URL(string: "https://example.com/image.png")! + XCTAssertEqual(try json(for: EncodingContainer(url)), #"{"value":"https:\/\/example.com\/image.png"}"#) + + // swiftlint:disable syntactic_sugar + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + XCTAssertEqual(try json(for: EncodingContainer(Optional.none)), #"{"value":null}"#) + // swiftlint:enable syntactic_sugar + } + + XCTAssertEqual(try json(for: [true, false, true, false]), "[true,false,true,false]") + XCTAssertEqual(try json(for: [1, 2, 3, 4, 5]), "[1,2,3,4,5]") + XCTAssertEqual(try json(for: [1.5, 2.5, 3.5]), "[1.5,2.5,3.5]") + XCTAssertEqual(try json(for: ["foo", "bar", "fizz", "buzz"]), #"["foo","bar","fizz","buzz"]"#) + + let foo = Foo(bar: "bar", bizz: .init(buzz: "buzz", bazz: [1: 2])) + XCTAssertEqual(try json(for: foo), #"{"bar":"bar","bizz":{"bazz":{"1":2},"buzz":"buzz"}}"#) + XCTAssertEqual(try json(for: [foo]), #"[{"bar":"bar","bizz":{"bazz":{"1":2},"buzz":"buzz"}}]"#) + XCTAssertEqual(try json(for: ["foo": foo]), #"{"foo":{"bar":"bar","bizz":{"bazz":{"1":2},"buzz":"buzz"}}}"#) + } + + func testGivenEncodedCodableValue_whenDecoding_itPreservesValueRepresentation() throws { + let encoder = JSONEncoder() + let decoder = JSONDecoder() + + func test(value: T) throws { + // Given + let codableValue = AnyEncodable(value) + let encodedCodableValue = try encoder.encode(codableValue) + + // When + let decodedCodableValue = try decoder.decode(AnyCodable.self, from: encodedCodableValue) + + // Then + DDAssertJSONEqual(codableValue, decodedCodableValue) + } + + try test(value: EncodingContainer(Bool.mockRandom())) + try test(value: EncodingContainer(UInt64.mockRandom())) + try test(value: EncodingContainer(Int.mockRandom())) + try test(value: EncodingContainer(Double.mockRandom())) + try test(value: EncodingContainer(String.mockRandom())) + try test(value: EncodingContainer(URL.mockRandom())) + try test(value: Foo.mockRandom()) + + // swiftlint:disable syntactic_sugar + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + try test(value: EncodingContainer(Optional.none)) + // swiftlint:enable syntactic_sugar + + try test(value: [Bool].mockRandom()) + try test(value: [UInt64].mockRandom()) + try test(value: [Int].mockRandom()) + try test(value: [Double].mockRandom()) + try test(value: [String].mockRandom()) + try test(value: [URL].mockRandom()) + try test(value: [Foo].mockRandom()) + + try test(value: [AttributeKey: Bool].mockRandom()) + try test(value: [AttributeKey: UInt64].mockRandom()) + try test(value: [AttributeKey: Int].mockRandom()) + try test(value: [AttributeKey: Double].mockRandom()) + try test(value: [AttributeKey: String].mockRandom()) + try test(value: [AttributeKey: URL].mockRandom()) + try test(value: [AttributeKey: Foo].mockRandom()) + } +} diff --git a/DatadogInternal/Tests/Codable/AnyCoderTests.swift b/DatadogInternal/Tests/Codable/AnyCoderTests.swift new file mode 100644 index 0000000000..08365e19a9 --- /dev/null +++ b/DatadogInternal/Tests/Codable/AnyCoderTests.swift @@ -0,0 +1,297 @@ +/* + * 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 + +private struct CodableObject: Codable, Equatable { + let id: UUID + let date: Date + let url: URL + let string: String + let null: String? + let integer: Int + let float: Float + let nested: Nested + let empty: Empty + let array: [Nested] + + struct Empty: Codable, Equatable { } + + struct Nested: Codable, Equatable { + let id: UUID + let string: String + } +} + +class AnyCoderTests: XCTestCase { + func testEncodingDecoding() throws { + let encoder = AnyEncoder() + let decoder = AnyDecoder() + + // Given + let expected: CodableObject = .mockRandom() + + // When + let any = try encoder.encode(expected) + let actual: CodableObject = try decoder.decode(from: any) + + // Then + XCTAssertEqual(actual, expected) + } + + func testSingleValueEncoding() throws { + let encoder = AnyEncoder() + XCTAssertTrue(try encoder.encode(true) is Bool) + XCTAssertTrue(try encoder.encode("str") is String) + XCTAssertTrue(try encoder.encode(Int(1)) is Int) + XCTAssertTrue(try encoder.encode(Int8(1)) is Int8) + XCTAssertTrue(try encoder.encode(Int16(1)) is Int16) + XCTAssertTrue(try encoder.encode(Int32(1)) is Int32) + XCTAssertTrue(try encoder.encode(Int64(1)) is Int64) + XCTAssertTrue(try encoder.encode(UInt(1)) is UInt) + XCTAssertTrue(try encoder.encode(UInt8(1)) is UInt8) + XCTAssertTrue(try encoder.encode(UInt16(1)) is UInt16) + XCTAssertTrue(try encoder.encode(UInt32(1)) is UInt32) + XCTAssertTrue(try encoder.encode(UInt64(1)) is UInt64) + XCTAssertTrue(try encoder.encode(Float(1.1)) is Float) + XCTAssertTrue(try encoder.encode(Double(1.1)) is Double) + } + + func testSingleValueDecoding() throws { + let decoder = AnyDecoder() + XCTAssertTrue(try decoder.decode(Bool.self, from: true)) + XCTAssertEqual(try decoder.decode(String.self, from: "str"), "str") + XCTAssertEqual(try decoder.decode(Int.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(Int8.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(Int16.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(Int32.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(Int64.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(UInt.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(UInt8.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(UInt16.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(UInt32.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(UInt64.self, from: 1), 1) + XCTAssertEqual(try decoder.decode(Float.self, from: Float(1)), 1) + XCTAssertEqual(try decoder.decode(Float.self, from: Double(1)), 1) + XCTAssertEqual(try decoder.decode(Float.self, from: Int(1)), 1) + XCTAssertEqual(try decoder.decode(Double.self, from: Double(1)), 1) + XCTAssertEqual(try decoder.decode(Double.self, from: Float(1)), 1) + XCTAssertEqual(try decoder.decode(Double.self, from: Int(1)), 1) + } + + func testUnkeyedEncoding() throws { + let encoder = AnyEncoder() + + struct Foo: Encodable { + func encode(to encoder: Encoder) throws { + var container1 = encoder.unkeyedContainer() + try container1.encodeNil() + try container1.encode(true) + try container1.encode("str") + try container1.encode(Int(1)) + try container1.encode(Int8(1)) + try container1.encode(Int16(1)) + try container1.encode(Int32(1)) + var container2 = encoder.unkeyedContainer() + try container2.encode(Int64(1)) + try container2.encode(UInt(1)) + try container2.encode(UInt8(1)) + try container2.encode(UInt16(1)) + try container2.encode(UInt32(1)) + try container2.encode(UInt64(1)) + try container2.encode(Float(1.1)) + try container2.encode(Double(1.1)) + var container3 = container2.nestedUnkeyedContainer() + try container3.encode("str") + var container4 = container2.nestedContainer(keyedBy: DynamicCodingKey.self) + try container4.encode("str", forKey: "str") + XCTAssertEqual(container2.count, 17) + } + } + + let array = try XCTUnwrap(try encoder.encode(Foo()) as? [Any?]) + XCTAssertNil(array[0]) + XCTAssertTrue(array[1] is Bool) + XCTAssertTrue(array[2] is String) + XCTAssertTrue(array[3] is Int) + XCTAssertTrue(array[4] is Int8) + XCTAssertTrue(array[5] is Int16) + XCTAssertTrue(array[6] is Int32) + XCTAssertTrue(array[7] is Int64) + XCTAssertTrue(array[8] is UInt) + XCTAssertTrue(array[9] is UInt8) + XCTAssertTrue(array[10] is UInt16) + XCTAssertTrue(array[11] is UInt32) + XCTAssertTrue(array[12] is UInt64) + XCTAssertTrue(array[13] is Float) + XCTAssertTrue(array[14] is Double) + XCTAssertTrue(array[15] is [String]) + XCTAssertTrue(array[16] is [String: String]) + } + + func testUnkeyedDecoding() throws { + let decoder = AnyDecoder() + + let array: [Any?] = [ + nil, + true, + "str", + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1.1, 1.1, + ["str"], + ["str": "str"], + ] + + struct Foo: Decodable { + init(from decoder: Decoder) throws { + XCTAssertThrowsError(try decoder.container(keyedBy: DynamicCodingKey.self)) + var container1 = try decoder.unkeyedContainer() + XCTAssertEqual(container1.count, 17) + XCTAssertTrue(try container1.decodeNil()) + XCTAssertTrue(try container1.decode(Bool.self)) + XCTAssertEqual(try container1.decode(String.self), "str") + XCTAssertEqual(try container1.decode(Int.self), 1) + XCTAssertEqual(try container1.decode(Int8.self), 1) + XCTAssertEqual(try container1.decode(Int16.self), 1) + XCTAssertEqual(try container1.decode(Int32.self), 1) + XCTAssertEqual(try container1.decode(Int64.self), 1) + XCTAssertEqual(try container1.decode(UInt.self), 1) + XCTAssertEqual(try container1.decode(UInt8.self), 1) + XCTAssertEqual(try container1.decode(UInt16.self), 1) + XCTAssertEqual(try container1.decode(UInt32.self), 1) + XCTAssertEqual(try container1.decode(UInt64.self), 1) + XCTAssertEqual(try container1.decode(Float.self), 1.1) + XCTAssertEqual(try container1.decode(Double.self), 1.1) + var container2 = try container1.nestedUnkeyedContainer() + XCTAssertEqual(try container2.decode(String.self), "str") + let container3 = try container1.nestedContainer(keyedBy: DynamicCodingKey.self) + XCTAssertEqual(try container3.decode(String.self, forKey: "str"), "str") + XCTAssertThrowsError(try container1.decodeNil()) + } + } + + XCTAssertNoThrow(try decoder.decode(Foo.self, from: array)) + } + + func testKeyedEncoding() throws { + let encoder = AnyEncoder() + + struct Foo: Encodable { + func encode(to encoder: Encoder) throws { + var container1 = encoder.container(keyedBy: DynamicCodingKey.self) + try container1.encodeNil(forKey: "null") + try container1.encode(true, forKey: "bool") + try container1.encode("str", forKey: "string") + try container1.encode(Int(1), forKey: "int") + try container1.encode(Int8(1), forKey: "int8") + try container1.encode(Int16(1), forKey: "int16") + try container1.encode(Int32(1), forKey: "int32") + try container1.encode(Int64(1), forKey: "int64") + var container2 = encoder.container(keyedBy: DynamicCodingKey.self) + try container2.encode(UInt(1), forKey: "uint") + try container2.encode(UInt8(1), forKey: "uint8") + try container2.encode(UInt16(1), forKey: "uint16") + try container2.encode(UInt32(1), forKey: "uint32") + try container2.encode(UInt64(1), forKey: "uint64") + try container2.encode(Float(1.1), forKey: "float") + try container2.encode(Double(1.1), forKey: "double") + var container3 = container2.nestedUnkeyedContainer(forKey: "array") + try container3.encode("str") + var container4 = container2.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: "nested") + try container4.encode("str", forKey: "str") + } + } + + let dictionary = try XCTUnwrap(try encoder.encode(Foo()) as? [String: Any?]) + XCTAssertNil(try XCTUnwrap(dictionary["null"])) + XCTAssertTrue(dictionary["bool"] is Bool) + XCTAssertTrue(dictionary["string"] is String) + XCTAssertTrue(dictionary["int"] is Int) + XCTAssertTrue(dictionary["int8"] is Int8) + XCTAssertTrue(dictionary["int16"] is Int16) + XCTAssertTrue(dictionary["int32"] is Int32) + XCTAssertTrue(dictionary["int64"] is Int64) + XCTAssertTrue(dictionary["uint"] is UInt) + XCTAssertTrue(dictionary["uint8"] is UInt8) + XCTAssertTrue(dictionary["uint16"] is UInt16) + XCTAssertTrue(dictionary["uint32"] is UInt32) + XCTAssertTrue(dictionary["uint64"] is UInt64) + XCTAssertTrue(dictionary["float"] is Float) + XCTAssertTrue(dictionary["double"] is Double) + XCTAssertTrue(dictionary["array"] is [String]) + XCTAssertTrue(dictionary["nested"] is [String: String]) + } + + func testKeyedDecoding() throws { + let decoder = AnyDecoder() + + let dictionary: [String: Any?] = [ + "null": nil, + "bool": true, + "string": "str", + "integer": 1, + "floating": 1.1, + "array": ["str"], + "nested": ["str": "str"], + ] + + struct Foo: Decodable { + init(from decoder: Decoder) throws { + XCTAssertThrowsError(try decoder.unkeyedContainer()) + let container1 = try decoder.container(keyedBy: DynamicCodingKey.self) + XCTAssertEqual(container1.allKeys.count, 7) + XCTAssertTrue(try container1.decodeNil(forKey: "null")) + XCTAssertTrue(try container1.decode(Bool.self, forKey: "bool")) + XCTAssertEqual(try container1.decode(String.self, forKey: "string"), "str") + XCTAssertEqual(try container1.decode(Int.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(Int8.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(Int16.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(Int32.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(Int64.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(UInt.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(UInt8.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(UInt16.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(UInt32.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(UInt64.self, forKey: "integer"), 1) + XCTAssertEqual(try container1.decode(Float.self, forKey: "floating"), 1.1) + XCTAssertEqual(try container1.decode(Double.self, forKey: "floating"), 1.1) + var container2 = try container1.nestedUnkeyedContainer(forKey: "array") + XCTAssertEqual(try container2.decode(String.self), "str") + let container3 = try container1.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: "nested") + XCTAssertEqual(try container3.decode(String.self, forKey: "str"), "str") + XCTAssertThrowsError(try container1.decodeNil(forKey: "unkown")) + } + } + + XCTAssertNoThrow(try decoder.decode(Foo.self, from: dictionary)) + } +} + +extension CodableObject: RandomMockable { + static func mockRandom() -> Self { + .init( + id: .mockRandom(), + date: .mockRandom(), + url: .mockRandom(), + string: .mockRandom(), + null: nil, + integer: .mockRandom(), + float: .mockRandom(), + nested: .mockRandom(), + empty: .init(), + array: .mockRandom() + ) + } +} + +extension CodableObject.Nested: RandomMockable { + fileprivate static func mockRandom() -> Self { + .init(id: .mockRandom(), string: .mockRandom()) + } +} diff --git a/DatadogInternal/Tests/Codable/AnyDecodableTests.swift b/DatadogInternal/Tests/Codable/AnyDecodableTests.swift new file mode 100644 index 0000000000..f1adc43eae --- /dev/null +++ b/DatadogInternal/Tests/Codable/AnyDecodableTests.swift @@ -0,0 +1,117 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +import XCTest +import DatadogInternal + +class AnyDecodableTests: XCTestCase { + func testJSONDecoding() throws { + let json = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "null": null + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let dictionary = try decoder.decode([String: AnyDecodable].self, from: json) + + XCTAssertEqual(dictionary["boolean"]?.value as? Bool, true) + XCTAssertEqual(dictionary["integer"]?.value as? Int, 42) + XCTAssertEqual(dictionary["double"]?.value as! Double, 3.141592653589793, accuracy: 0.001) + XCTAssertEqual(dictionary["string"]?.value as? String, "string") + XCTAssertEqual(dictionary["array"]?.value as? [Int], [1, 2, 3]) + XCTAssertEqual(dictionary["nested"]?.value as? [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) + XCTAssertEqual(dictionary["null"]?.value as? NSNull, NSNull()) + } + + func testAnyDecoding() throws { + class Passthrough: PassthroughAnyCodable { + init() {} + } + + let passthrough = Passthrough() + + let any: [String: Any?] = [ + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": [ + "a": "alpha", + "b": "bravo", + "c": "charlie" + ], + "null": nil, + "uuid": UUID(), + "url": URL(string: "https://test.com"), + "passthrough": passthrough + ] + + let decoder = AnyDecoder() + let dictionary = try decoder.decode([String: AnyDecodable].self, from: any) + + XCTAssertEqual(dictionary["boolean"]?.value as? Bool, true) + XCTAssertEqual(dictionary["integer"]?.value as? Int, 42) + XCTAssertEqual(dictionary["double"]?.value as! Double, 3.141592653589793, accuracy: 0.001) + XCTAssertEqual(dictionary["string"]?.value as? String, "string") + XCTAssertEqual(dictionary["array"]?.value as? [Int], [1, 2, 3]) + XCTAssertEqual(dictionary["nested"]?.value as? [String: String], ["a": "alpha", "b": "bravo", "c": "charlie"]) + XCTAssert(dictionary["uuid"]?.value is UUID) + XCTAssertEqual(dictionary["url"]?.value as? URL, URL(string: "https://test.com")) + XCTAssert(dictionary["passthrough"]?.value as? Passthrough === passthrough) + } + + func testAnyDecodingFailue() throws { + class NotPassthrough { + init() {} + } + + let passthrough = NotPassthrough() + + let any: [String: Any?] = [ + "passthrough": passthrough + ] + + let decoder = AnyDecoder() + XCTAssertThrowsError(try decoder.decode(AnyDecodable.self, from: any)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(context.debugDescription, "AnyDecodable value cannot be decoded") + } + } +} diff --git a/DatadogInternal/Tests/Codable/AnyEncodableTests.swift b/DatadogInternal/Tests/Codable/AnyEncodableTests.swift new file mode 100644 index 0000000000..cd418da75a --- /dev/null +++ b/DatadogInternal/Tests/Codable/AnyEncodableTests.swift @@ -0,0 +1,186 @@ +/* + * 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. + * + * This file includes software developed by Flight School, https://flight.school/ and altered by Datadog. + * Use of this source code is governed by MIT license: + * + * Copyright 2018 Read Evaluate Press, LLC + * + * 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. + */ + +import XCTest +import DatadogInternal + +class AnyEncodableTests: XCTestCase { + struct SomeEncodable: Encodable { + var string: String + var int: Int + var bool: Bool + var hasUnderscore: String + + enum CodingKeys: String,CodingKey { + case string + case int + case bool + case hasUnderscore = "has_underscore" + } + } + + func testJSONEncoding() throws { + let someEncodable = AnyEncodable( + SomeEncodable( + string: "String", + int: 100, + bool: true, + hasUnderscore: "another string" + ) + ) + + let dictionary: [String: Any?] = [ + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": [ + "a": "alpha", + "b": "bravo", + "c": "charlie", + ], + "someCodable": someEncodable, + "null": nil, + "url": URL(string: "https://example.com/image.png")! + ] + + let encoder = JSONEncoder() + + let json = try encoder.encode(AnyEncodable(dictionary)) + let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary + + let expected = """ + { + "boolean": true, + "integer": 42, + "double": 3.141592653589793, + "string": "string", + "array": [1, 2, 3], + "nested": { + "a": "alpha", + "b": "bravo", + "c": "charlie" + }, + "someCodable": { + "string":"String", + "int":100, + "bool": true, + "has_underscore":"another string" + }, + "null": null, + "url": "https://example.com/image.png" + } + """.data(using: .utf8)! + let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary + + XCTAssertEqual(encodedJSONObject, expectedJSONObject) + } + + func testEncodeNSNumber() throws { + let dictionary: [String: NSNumber] = [ + "boolean": true, + "char": -127, + "int": -32_767, + "short": -32_767, + "long": -2_147_483_647, + "longlong": -9_223_372_036_854_775_807, + "uchar": 255, + "uint": 65_535, + "ushort": 65_535, + "ulong": 4_294_967_295, + "ulonglong": 18_446_744_073_709_615, + "double": 3.141592653589793, + ] + + let encoder = JSONEncoder() + + let json = try encoder.encode(AnyEncodable(dictionary)) + let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary + + let expected = """ + { + "boolean": true, + "char": -127, + "int": -32767, + "short": -32767, + "long": -2147483647, + "longlong": -9223372036854775807, + "uchar": 255, + "uint": 65535, + "ushort": 65535, + "ulong": 4294967295, + "ulonglong": 18446744073709615, + "double": 3.141592653589793, + } + """.data(using: .utf8)! + let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary + + XCTAssertEqual(encodedJSONObject, expectedJSONObject) + XCTAssert(encodedJSONObject["boolean"] is Bool) + + XCTAssert(encodedJSONObject["char"] is Int8) + XCTAssert(encodedJSONObject["int"] is Int16) + XCTAssert(encodedJSONObject["short"] is Int32) + XCTAssert(encodedJSONObject["long"] is Int32) + XCTAssert(encodedJSONObject["longlong"] is Int64) + + XCTAssert(encodedJSONObject["uchar"] is UInt8) + XCTAssert(encodedJSONObject["uint"] is UInt16) + XCTAssert(encodedJSONObject["ushort"] is UInt32) + XCTAssert(encodedJSONObject["ulong"] is UInt32) + XCTAssert(encodedJSONObject["ulonglong"] is UInt64) + + XCTAssert(encodedJSONObject["double"] is Double) + } + + func testStringInterpolationEncoding() throws { + let dictionary: [String: Any] = [ + "boolean": "\(true)", + "integer": "\(42)", + "double": "\(3.141592653589793)", + "string": "\("string")", + "array": "\([1, 2, 3])", + ] + + let encoder = JSONEncoder() + + let json = try encoder.encode(AnyEncodable(dictionary)) + let encodedJSONObject = try JSONSerialization.jsonObject(with: json, options: []) as! NSDictionary + + let expected = """ + { + "boolean": "true", + "integer": "42", + "double": "3.141592653589793", + "string": "string", + "array": "[1, 2, 3]", + } + """.data(using: .utf8)! + let expectedJSONObject = try JSONSerialization.jsonObject(with: expected, options: []) as! NSDictionary + + XCTAssertEqual(encodedJSONObject, expectedJSONObject) + } +} diff --git a/DatadogInternal/Tests/Concurrency/ReadWriteLockTests.swift b/DatadogInternal/Tests/Concurrency/ReadWriteLockTests.swift new file mode 100644 index 0000000000..b9251e1740 --- /dev/null +++ b/DatadogInternal/Tests/Concurrency/ReadWriteLockTests.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 XCTest +import DatadogInternal + +final class ReadWriteLockTests: XCTestCase { + @ReadWriteLock + var value: Int = 0 + + func testRandomlyCallingValueConcurrentlyDoesNotCrash() { + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { _ = self.value }, + { self.value = .mockRandom() }, + { self._value.mutate { $0 = .mockRandom() } } + ], + iterations: 1_000 + ) + // swiftlint:enable opening_brace + } +} diff --git a/DatadogInternal/Tests/Context/AppStateHistoryTests.swift b/DatadogInternal/Tests/Context/AppStateHistoryTests.swift new file mode 100644 index 0000000000..4882207065 --- /dev/null +++ b/DatadogInternal/Tests/Context/AppStateHistoryTests.swift @@ -0,0 +1,126 @@ +/* + * 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 + +class AppStateHistoryTests: XCTestCase { + func testItBuildsAppStateFromUIApplicationState() { + XCTAssertEqual(AppState(.active), .active) + XCTAssertEqual(AppState(.inactive), .inactive) + XCTAssertEqual(AppState(.background), .background) + } + + func testWhenAppTransitionsBetweenForegroundAndBackground_itComputesTotalForegroundDuration() { + let numberOfForegroundSnapshots: Int = .mockRandom(min: 0, max: 20) + let numberOfBackgroundSnapshots: Int = .mockRandom(min: numberOfForegroundSnapshots == 0 ? 1 : 0, max: 20) // must include at least one snapshot + let numberOfSnapshots = numberOfForegroundSnapshots + numberOfBackgroundSnapshots + let snapshotDuration: TimeInterval = .mockRandom(min: 0.1, max: 5) + + // Given + let date: Date = .mockRandomInThePast() + let snapshotDates: [Date] = (0.. 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 new file mode 100644 index 0000000000..aed89048bc --- /dev/null +++ b/DatadogInternal/Tests/CoreRegistryTest.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 +@testable import TestUtilities + +@testable import DatadogInternal + +class CoreRegistryTest: XCTestCase { + func testRegistration() { + let core = PassthroughCoreMock() + CoreRegistry.register(default: core) + XCTAssertTrue(CoreRegistry.default === core) + + let name: String = .mockRandom() + CoreRegistry.register(core, named: name) + XCTAssertTrue(CoreRegistry.instance(named: name) === core) + XCTAssertTrue(CoreRegistry.isRegistered(instanceName: CoreRegistry.defaultInstanceName)) + XCTAssertTrue(CoreRegistry.isRegistered(instanceName: name)) + + CoreRegistry.unregisterDefault() + CoreRegistry.unregisterInstance(named: name) + XCTAssertTrue(CoreRegistry.default is NOPDatadogCore) + XCTAssertTrue(CoreRegistry.instance(named: name) is NOPDatadogCore) + XCTAssertFalse(CoreRegistry.isRegistered(instanceName: CoreRegistry.defaultInstanceName)) + XCTAssertFalse(CoreRegistry.isRegistered(instanceName: name)) + } + + func testConcurrency() { + let core = PassthroughCoreMock() + + // swiftlint:disable opening_brace + callConcurrently( + { CoreRegistry.register(default: core) }, + { _ = CoreRegistry.default }, + { CoreRegistry.unregisterDefault() }, + { CoreRegistry.register(core, named: "test") }, + { _ = CoreRegistry.instance(named: "test") }, + { CoreRegistry.unregisterInstance(named: "test") } + ) + // swiftlint:enable opening_brace + + 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/DatadogCoreProtocolTests.swift b/DatadogInternal/Tests/DatadogCoreProtocolTests.swift new file mode 100644 index 0000000000..707bf6f934 --- /dev/null +++ b/DatadogInternal/Tests/DatadogCoreProtocolTests.swift @@ -0,0 +1,56 @@ +/* + * 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 DatadogCoreProtocolTests: XCTestCase { + func testSendMessageExtension() throws { + // Given + let receiver = FeatureMessageReceiverMock() + let core = PassthroughCoreMock(messageReceiver: receiver) + + // When + core.send(message: .baggage(key: "test", value: "value")) + + // Then + XCTAssertEqual( + try receiver.messages.last?.baggage(forKey: "test"), "value", "DatadogCoreProtocol.send(message:) should forward message" + ) + } + + func testSetBaggageExtension() throws { + // Given + let core = PassthroughCoreMock() + + // Then + core.set(baggage: FeatureBaggage("value"), forKey: "test") + XCTAssertEqual( + try core.context.baggages["test"]?.decode(), "value", "DatadogCoreProtocol.set(baggage:) should forward baggage" + ) + + core.set(baggage: nil, forKey: "test") + XCTAssertNil(core.context.baggages["test"], "DatadogCoreProtocol.set(baggage:) should forward baggage" ) + + core.set(baggage: { "value" }, forKey: "test") + XCTAssertEqual( + try core.context.baggages["test"]?.decode(), "value", "DatadogCoreProtocol.set(baggage:) should forward baggage" + ) + + core.set(baggage: { nil as String? }, forKey: "test") + XCTAssertNil(core.context.baggages["test"], "DatadogCoreProtocol.set(baggage:) should forward baggage" ) + + core.set(baggage: "value", forKey: "test") + XCTAssertEqual( + try core.context.baggages["test"]?.decode(), "value", "DatadogCoreProtocol.set(baggage:) should forward baggage" + ) + + core.set(baggage: nil as String?, forKey: "test") + XCTAssertNil(core.context.baggages["test"], "DatadogCoreProtocol.set(baggage:) should forward baggage" ) + } +} 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/Extensions/FixedWidthInteger+ConvenienceTests.swift b/DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.swift new file mode 100644 index 0000000000..25fa315150 --- /dev/null +++ b/DatadogInternal/Tests/Extensions/FixedWidthInteger+ConvenienceTests.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 +@testable import DatadogInternal + +final class FixedWidthIntegerConvenienceTests: XCTestCase { + func test_Bytes() { + let value: Int = 1_000 + XCTAssertEqual(value.bytes, 1_000) + } + + func test_Kilobytes() { + let value: Int = 1 + XCTAssertEqual(value.KB, 1_024) + } + + func test_Megabytes() { + let value: Int = 1 + XCTAssertEqual(value.MB, 1_048_576) + } + + func test_Gigabytes() { + let value: Int = 1 + XCTAssertEqual(value.GB, 1_073_741_824) + } + + func test_OverflowKilobytes() { + let value = UInt64.max / 1_024 + XCTAssertEqual(value.KB, UInt64.max &- 1_023) + } + + func test_OverflowMegabytes() { + let value = UInt64.max / (1_024 * 1_024) + XCTAssertEqual(value.MB, UInt64.max &- 1_048_575) + } + + func test_OverflowGigabytes() { + let value = UInt64.max / (1_024 * 1_024 * 1_024) + XCTAssertEqual(value.GB, UInt64.max &- 1_073_741_823) + } +} diff --git a/DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift b/DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift new file mode 100644 index 0000000000..01707eb603 --- /dev/null +++ b/DatadogInternal/Tests/Extensions/TimeInterval+ConvenienceTests.swift @@ -0,0 +1,42 @@ +/* + * 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 + +final class TimeIntervalConvenienceTests: XCTestCase { + func test_Seconds() { + XCTAssertEqual(TimeInterval(30).seconds, 30) + XCTAssertEqual(Int(30).seconds, 30) + } + + func test_Minutes() { + XCTAssertEqual(TimeInterval(2).minutes, 120) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Hours() { + XCTAssertEqual(TimeInterval(3).hours, 10_800) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Days() { + XCTAssertEqual(TimeInterval(1).days, 86_400) + XCTAssertEqual(Int(2).minutes, 120) + } + + func test_Overflow() { + let timeInterval = TimeInterval.greatestFiniteMagnitude + XCTAssertEqual(timeInterval.minutes, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(timeInterval.hours, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(timeInterval.days, TimeInterval.greatestFiniteMagnitude) + + let integerTimeInterval = Int.max + XCTAssertEqual(integerTimeInterval.minutes, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(integerTimeInterval.hours, TimeInterval.greatestFiniteMagnitude) + XCTAssertEqual(integerTimeInterval.days, TimeInterval.greatestFiniteMagnitude) + } +} diff --git a/DatadogInternal/Tests/MessageBus/FeatureBaggageTests.swift b/DatadogInternal/Tests/MessageBus/FeatureBaggageTests.swift new file mode 100644 index 0000000000..9fa42a5d47 --- /dev/null +++ b/DatadogInternal/Tests/MessageBus/FeatureBaggageTests.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 + +class FeatureBaggageTests: XCTestCase { + struct GroceryProduct: Encodable, RandomMockable { + var name: String + var points: Int + var description: String + + static func mockRandom() -> Self { + .init( + name: .mockRandom(), + points: .mockRandom(), + description: .mockRandom() + ) + } + } + + struct CartItem: Decodable { + var name: String + var points: Int + var code: String? + } + + func testEncodeDecode() throws { + let pear = GroceryProduct.mockRandom() + let baggage = FeatureBaggage(pear) + let item: CartItem = try baggage.decode() + + XCTAssertEqual(pear.name, item.name) + XCTAssertEqual(pear.points, item.points) + XCTAssertNil(item.code) + } + + func testEncodingFailure() throws { + struct FaultyEncodable: Encodable { + func encode(to encoder: Encoder) throws { + throw EncodingError.invalidValue( + self, + .init(codingPath: [], debugDescription: "FaultyEncodable") + ) + } + } + + let faulty = FaultyEncodable() + let baggage = FeatureBaggage(faulty) + XCTAssertThrowsError(try baggage.decode(type: CartItem.self)) { + XCTAssert($0 is EncodingError) + } + } + + func testDecodingFailure() throws { + struct FaultyDecodable: Decodable { + init(from decoder: Decoder) throws { + throw DecodingError.valueNotFound( + FaultyDecodable.self, + .init(codingPath: [], debugDescription: "FaultyDecodable") + ) + } + } + + let pear = GroceryProduct.mockRandom() + let baggage = FeatureBaggage(pear) + XCTAssertThrowsError(try baggage.decode(type: FaultyDecodable.self)) { error in + XCTAssert(error is DecodingError) + } + } + + func testThreadSafety() { + let pear = GroceryProduct.mockRandom() + let baggage = FeatureBaggage(pear) + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { _ = try? baggage.encode() }, + { _ = try? baggage.decode(type: CartItem.self) } + ], + iterations: 100 + ) + // swiftlint:enable opening_brace + } +} diff --git a/DatadogInternal/Tests/MessageBus/FeatureMessageReceiverTests.swift b/DatadogInternal/Tests/MessageBus/FeatureMessageReceiverTests.swift new file mode 100644 index 0000000000..b27365438f --- /dev/null +++ b/DatadogInternal/Tests/MessageBus/FeatureMessageReceiverTests.swift @@ -0,0 +1,71 @@ +/* + * 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 +import TestUtilities + +class FeatureMessageReceiverTests: XCTestCase { + private var core: PassthroughCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = PassthroughCoreMock() + } + + override func tearDown() { + core = nil + super.tearDown() + } + + struct TestReceiver: FeatureMessageReceiver { + let expectation: XCTestExpectation? + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + expectation?.fulfill() + return true + } + } + + func testNOPReceiver_returnsFalse() throws { + let receiver = NOPFeatureMessageReceiver() + XCTAssertFalse(receiver.receive(message: .baggage(key: .mockAny(), value: "test"), from: core)) + XCTAssertFalse(receiver.receive(message: .context(.mockRandom()), from: core)) + } + + func testEmptyCombinedReceiver_returnsFalse() throws { + let receiver = CombinedFeatureMessageReceiver([]) + XCTAssertFalse(receiver.receive(message: .baggage(key: .mockAny(), value: "test"), from: core)) + XCTAssertFalse(receiver.receive(message: .context(.mockRandom()), from: core)) + } + + func testCombinedReceiver_withValidReceiver_returnsTrue() throws { + let expectation = expectation(description: "receive 2 messages") + expectation.expectedFulfillmentCount = 2 + + let receiver = CombinedFeatureMessageReceiver( + NOPFeatureMessageReceiver(), + TestReceiver(expectation: expectation) + ) + + XCTAssertTrue(receiver.receive(message: .baggage(key: .mockAny(), value: "test"), from: core)) + XCTAssertTrue(receiver.receive(message: .context(.mockRandom()), from: core)) + waitForExpectations(timeout: 0) + } + + func testCombinedReceiver_withMultiValidReceiver_itSendsToFirstOnly() throws { + let expectation = self.expectation(description: "receive message") + let noExpectation = self.expectation(description: "do not receive message") + noExpectation.isInverted = true + + let receiver = CombinedFeatureMessageReceiver( + TestReceiver(expectation: expectation), + TestReceiver(expectation: noExpectation) + ) + + XCTAssertTrue(receiver.receive(message: .baggage(key: .mockAny(), value: "test"), from: core)) + waitForExpectations(timeout: 0) + } +} diff --git a/DatadogInternal/Tests/Models/WebViewMessageTests.swift b/DatadogInternal/Tests/Models/WebViewMessageTests.swift new file mode 100644 index 0000000000..3731846afe --- /dev/null +++ b/DatadogInternal/Tests/Models/WebViewMessageTests.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 XCTest +import TestUtilities + +@testable import DatadogInternal + +class WebViewMessageTests: XCTestCase { + let decoder = JSONDecoder() + + func testParsingCorruptedEvent() throws { + let invalidJSON = "(^#$@#)".utf8Data + + XCTAssertThrowsError(try decoder.decode(WebViewMessage.self, from: invalidJSON)) { error in + XCTAssert(error is DecodingError) + } + } + + func testParsingInvalidEvent() { + let messageWithNoEventType = """ + { + "event": { + "date": 1635932927012, + "error": { + "origin": "console" + } + } + } + """.utf8Data + + let messageWithNoEvent = """ + { + "eventType": "log" + } + """.utf8Data + + XCTAssertThrowsError(try decoder.decode(WebViewMessage.self, from: messageWithNoEventType)) { error in + XCTAssert(error is DecodingError) + } + + XCTAssertThrowsError(try decoder.decode(WebViewMessage.self, from: messageWithNoEvent)) { error in + XCTAssert(error is DecodingError) + } + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift new file mode 100644 index 0000000000..8bfcab0ca9 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -0,0 +1,111 @@ +/* + * 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 + +class B3HTTPHeadersReaderTests: XCTestCase { + func testItReadsSingleHeader() { + let reader = B3HTTPHeadersReader(httpHeaderFields: ["b3": "4d2-929-1-162e"]) + + let ids = reader.read() + + XCTAssertEqual(ids?.traceID, 1_234) + XCTAssertEqual(ids?.spanID, 2_345) + XCTAssertEqual(ids?.parentSpanID, 5_678) + } + + func testItReadsSingleHeaderWithSampling() { + let reader = B3HTTPHeadersReader(httpHeaderFields: ["b3": "0"]) + + let ids = reader.read() + + XCTAssertNil(ids?.traceID) + XCTAssertNil(ids?.spanID) + XCTAssertNil(ids?.parentSpanID) + } + + func testItReadsSingleHeaderWithoutOptionalValues() { + let reader = B3HTTPHeadersReader(httpHeaderFields: ["b3": "4d2-929"]) + + let ids = reader.read() + + XCTAssertEqual(ids?.traceID, 1_234) + XCTAssertEqual(ids?.spanID, 2_345) + XCTAssertNil(ids?.parentSpanID) + } + + func testItReadsMultipleHeader() { + let reader = B3HTTPHeadersReader(httpHeaderFields: [ + "X-B3-TraceId": "4d2", + "X-B3-SpanId": "929", + "X-B3-Sampled": "1", + "X-B3-ParentSpanId": "162e" + ]) + + let ids = reader.read() + + XCTAssertEqual(ids?.traceID, 1_234) + XCTAssertEqual(ids?.spanID, 2_345) + XCTAssertEqual(ids?.parentSpanID, 5_678) + } + + func testItReadsMultipleHeaderWithSampling() { + let reader = B3HTTPHeadersReader(httpHeaderFields: [ + "X-B3-Sampled": "0" + ]) + + let ids = reader.read() + + XCTAssertNil(ids?.traceID) + XCTAssertNil(ids?.spanID) + XCTAssertNil(ids?.parentSpanID) + } + + func testItReadsMultipleHeaderWithoutOptionalValues() { + let reader = B3HTTPHeadersReader(httpHeaderFields: [ + "X-B3-TraceId": "4d2", + "X-B3-SpanId": "929" + ]) + + let ids = reader.read() + + XCTAssertEqual(ids?.traceID, 1_234) + XCTAssertEqual(ids?.spanID, 2_345) + XCTAssertNil(ids?.parentSpanID) + } + + func testReadingSampledTraceContext() { + let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), injectEncoding: encoding, traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { + let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding, traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding, traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertNil(reader.sampled) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift new file mode 100644 index 0000000000..74bad2fb05 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -0,0 +1,224 @@ +/* + * 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 + +class B3HTTPHeadersWriterTests: XCTestCase { + func testWritingSampledTraceContext_withSingleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .single, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") + } + + func testWritingDroppedTraceContext_withSingleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .single, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + } + + func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .single, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") + } + + func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + injectEncoding: .single, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + } + + func testItWritesSingleHeaderWithoutOptionalValues() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .single, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1") + } + + func testWritingSampledTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .multiple, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "1") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") + } + + func testWritingDroppedTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .multiple, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) + XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") + XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + } + + func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .multiple, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "1") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") + } + + func testWritingDroppedTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + injectEncoding: .multiple, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) + XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") + XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + } + + func testItWritesMultipleHeaderWithoutOptionalValues() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .headBased, + injectEncoding: .multiple, + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "1") + XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/FirstPartyHostsTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/FirstPartyHostsTests.swift new file mode 100644 index 0000000000..d92d338c23 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/FirstPartyHostsTests.swift @@ -0,0 +1,154 @@ +/* + * 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 FirstPartyHostsTests: XCTestCase { + let hostsDictionary: [String: Set] = [ + "http://first-party.com/": [.tracecontext, .b3], + "https://first-party.com/": [.tracecontext, .b3], + "https://api.first-party.com/v2/users": [.tracecontext, .b3], + "https://www.first-party.com/": [.tracecontext, .b3], + "https://login:p4ssw0rd@first-party.com:999/": [.tracecontext, .b3], + "http://any-domain.eu/": [.tracecontext, .b3], + "https://any-domain.eu/": [.tracecontext, .b3], + "https://api.any-domain.eu/v2/users": [.tracecontext, .b3], + "https://www.any-domain.eu/": [.tracecontext, .b3], + "https://login:p4ssw0rd@www.any-domain.eu:999/": [.tracecontext, .b3], + "https://api.any-domain.org.eu/": [.tracecontext, .b3], + ] + + let otherHosts = [ + "http://third-party.com/", + "https://third-party.com/", + "https://api.third-party.com/v2/users", + "https://www.third-party.com/", + "https://login:p4ssw0rd@third-party.com:999/", + "http://any-domain.org/", + "https://any-domain.org/", + "https://api.any-domain.org/v2/users", + "https://www.any-domain.org/", + "https://login:p4ssw0rd@www.any-domain.org:999/", + "https://api.any-domain.eu.org/", + ] + + func testGivenEmptyDictionary_itReturnsDefaultTracingHeaderTypes() { + let headerTypesProvider = FirstPartyHosts() + (hostsDictionary.keys + otherHosts).forEach { fixture in + let url = URL(string: fixture) + XCTAssertEqual(headerTypesProvider.tracingHeaderTypes(for: url), .init()) + } + XCTAssertTrue(headerTypesProvider.hosts.isEmpty) + } + + func testGivenEmptyTracingHeaderTypes_itReturnsNoTracingHeaderTypes() { + let headerTypesProvider = FirstPartyHosts( + ["http://first-party.com/": .init()] + ) + XCTAssertEqual(headerTypesProvider.tracingHeaderTypes(for: URL(string: "http://first-party.com/")), .init()) + } + + func testGivenValidDictionary_itReturnsTracingHeaderTypes_forSubdomainURL() { + let firstPartyHosts = FirstPartyHosts([ + "first-party.com": .init([.b3multi]), + "example.com": [.datadog, .b3multi], + "subdomain.example.com": [.tracecontext], + "otherdomain.com": [.b3] + ]) + + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "http://example.com/path1")), [.datadog, .b3multi]) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "https://subdomain.example.com/path2")), [.tracecontext, .datadog, .b3multi]) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "http://otherdomain.com/path3")), [.b3]) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "https://somedomain.com/path4")), []) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "http://api.first-party.com")), [.b3multi]) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "http://apifirst-party.com")), []) + XCTAssertEqual(firstPartyHosts.tracingHeaderTypes(for: URL(string: "https://api.first-party.com/v1/endpoint")), [.b3multi]) + } + + func testGivenValidDictionary_itReturnsCorrectTracingHeaderTypes() { + let headerTypesProvider = FirstPartyHosts(hostsDictionary) + hostsDictionary.keys.forEach { fixture in + let url = URL(string: fixture) + XCTAssertEqual(headerTypesProvider.tracingHeaderTypes(for: url), [.tracecontext, .b3]) + } + otherHosts.forEach { fixture in + let url = URL(string: fixture) + XCTAssertEqual(headerTypesProvider.tracingHeaderTypes(for: url), .init()) + } + } + + func testGivenValidSet_itAssignsDatadogAndTracecontextHeaderType() { + let hosts = FirstPartyHosts(Set(otherHosts)) + otherHosts.forEach { + let url = URL(string: $0) + XCTAssertEqual(hosts.tracingHeaderTypes(for: url), [.datadog, .tracecontext]) + } + } + + func testFalsePositiveURL_itReturnsEmptyTracingHeaderTypes() { + let filter = FirstPartyHosts( + hostsWithTracingHeaderTypes: ["example.com": [.datadog, .b3multi]] + ) + let url = URL(string: "http://foo.com/something.example.com") + + XCTAssertEqual(filter.tracingHeaderTypes(for: url), []) + } + + func testGivenFilterIsInitializedWithEmptySet_itNeverReturnsFirstParty() { + let filter = FirstPartyHosts([:]) + (hostsDictionary.keys + otherHosts).forEach { fixture in + let url = URL(string: fixture)! + XCTAssertFalse( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } + } + + func testGivenURLHostIsSubdomain_itIsConsideredFirstParty() { + let filter = FirstPartyHosts([ + "first-party.com": .init([.datadog]) + ]) + let url = URL(string: "https://api.first-party.com")! + XCTAssertTrue( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + } + + func testGivenURLHostIsNotSubdomain_itIsNotConsideredFirstParty() { + let filter = FirstPartyHosts([ + "first-party.com": .init([.datadog]) + ]) + let urlString = "https://apifirst-party.com" + let url = URL(string: urlString)! + XCTAssertFalse( + filter.isFirstParty(url: url), + "The url: `\(url)` should NOT be matched as first party." + ) + XCTAssertFalse( + filter.isFirstParty(string: urlString), + "The url: `\(urlString)` should NOT be matched as first party." + ) + } + + func testGivenWRongURL_itIsNotConsideredFirstParty() { + let filter = FirstPartyHosts([ + "first-party.com": .init([.datadog]) + ]) + let badUrlString = "" + let badUrl = URL(string: badUrlString) + XCTAssertFalse( + filter.isFirstParty(url: badUrl), + "The url: `\(String(describing: badUrl))` should NOT be matched as first party." + ) + XCTAssertFalse( + filter.isFirstParty(string: badUrlString), + "The url: `\(badUrlString)` should NOT be matched as first party." + ) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift new file mode 100644 index 0000000000..fc313b729e --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift @@ -0,0 +1,38 @@ +/* + * 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 HTTPHeadersReaderTests: XCTestCase { + func testReadingSampledTraceContext() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNotNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertNil(reader.sampled) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift new file mode 100644 index 0000000000..1a526f6ae8 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift @@ -0,0 +1,83 @@ +/* + * 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 + +class HTTPHeadersWriterTests: XCTestCase { + func testWritingSampledTraceContext_withHeadBasedSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .all) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "1234") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "2345") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withHeadBasedSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .headBased, traceContextInjection: .sampled) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[TracingHTTPHeaders.samplingPriorityField]) + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "1234") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "2345") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[TracingHTTPHeaders.samplingPriorityField]) + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.swift new file mode 100644 index 0000000000..1eac303083 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/HostsSanitizerTests.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 XCTest +import TestUtilities +@testable import DatadogInternal + +class HostsSanitizerTests: XCTestCase { + func testSanitizationAndWarningMessages() throws { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { message, _ in print(message) } } + + // When + let hosts: Set = [ + "https://first-party.com", // sanitize to → "first-party.com" + "http://api.first-party.com", // sanitize to → "api.first-party.com" + "https://first-party.com/v2/api", // sanitize to → "first-party.com" + "https://192.168.0.1/api", // sanitize to → "192.168.0.1" + "https://192.168.0.2", // sanitize to → "192.168.0.2" + "invalid-host-name", // drop + "192.168.0.3:8080", // drop + "", // drop + "localhost", // accept + "192.168.0.4", // accept + "valid-host-name.com", // accept + ] + + // Then + let sanitizer = HostsSanitizer() + let sanitizedHosts = sanitizer.sanitized(hosts: hosts, warningMessage: "Host is not valid") + + XCTAssertEqual( + sanitizedHosts, + [ + "first-party.com", + "api.first-party.com", + "localhost", + "192.168.0.1", + "192.168.0.2", + "localhost", + "192.168.0.4", + "valid-host-name.com" + ] + ) + + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: '192.168.0.3:8080' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: '' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://first-party.com' is an url and will be sanitized to: 'first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://192.168.0.1/api' is an url and will be sanitized to: '192.168.0.1'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'http://api.first-party.com' is an url and will be sanitized to: 'api.first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://first-party.com/v2/api' is an url and will be sanitized to: 'first-party.com'.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'invalid-host-name' is not a valid host name and will be dropped.") + ) + XCTAssertTrue( + printFunction.printedMessages.contains("⚠️ Host is not valid: 'https://192.168.0.2' is an url and will be sanitized to: '192.168.0.2'.") + ) + XCTAssertEqual(printFunction.printedMessages.count, 8) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift new file mode 100644 index 0000000000..d805579bcc --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/ImmutableRequestTests.swift @@ -0,0 +1,40 @@ +/* + * 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 + +class ImmutableRequestTests: XCTestCase { + func testReadingURL() { + let original: URLRequest = .mockWith(url: "https://example.com") + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.url, original.url) + } + + func testReadingHTTPMethod() { + let original: URLRequest = .mockWith(httpMethod: .mockRandom()) + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.httpMethod, original.httpMethod) + } + + func testReadingDatadogOriginHeader() { + let expectedValue: String = .mockRandom(length: 128) + let original: URLRequest = .mockWith( + headers: [ + TracingHTTPHeaders.originField: expectedValue + ] + ) + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.ddOriginHeaderValue, expectedValue) + } + + func testPreservingUnsafeOriginal() { + let original: URLRequest = .mockAny() + let immutable = ImmutableRequest(request: original) + XCTAssertEqual(immutable.unsafeOriginal, original) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift new file mode 100644 index 0000000000..40693b5166 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift @@ -0,0 +1,641 @@ +/* + * 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 NetworkInstrumentationFeatureTests: 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() + } + + // MARK: - Interception Flow + + func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURL_itNotifiesInterceptor() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + + handler.onInterceptionDidStart = { _ in + notifyInterceptionDidStart.fulfill() + } + handler.onInterceptionDidComplete = { _ in + notifyInterceptionDidComplete.fulfill() + } + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + let task = session.dataTask(with: URL.mockAny()) + task.resume() + + // Then + wait( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + _ = server.waitAndReturnRequests(count: 1) + } + + func testGivenURLSessionWithDatadogDelegate_whenUsingTaskWithURLRequest_itNotifiesInterceptor() throws { + let notifyRequestMutation = expectation(description: "Notify request mutation") + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + + handler.onRequestMutation = { _, _ in notifyRequestMutation.fulfill() } + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + // Given + let url: URL = .mockAny() + handler.firstPartyHosts = .init( + hostsWithTracingHeaderTypes: [url.host!: [.datadog]] + ) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + session + .dataTask(with: URLRequest(url: url)) + .resume() + + // Then + wait( + for: [ + notifyRequestMutation, + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + _ = server.waitAndReturnRequests(count: 1) + } + + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncDataFromURL_itNotifiesInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock( + delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10)), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + _ = try await session.data(from: URL.mockAny(), delegate: delegate) + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + + _ = server.waitAndReturnRequests(count: 1) + } + + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncDataForURLRequest_itNotifiesInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + let server = ServerMock( + delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10)), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { interception in + XCTAssertTrue(interception.isFirstPartyRequest) + notifyInterceptionDidStart.fulfill() + } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + // Given + let url: URL = .mockAny() + handler.firstPartyHosts = .init( + hostsWithTracingHeaderTypes: [url.host!: [.datadog]] + ) + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + _ = try await session.data(for: URLRequest(url: url), delegate: delegate) + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + + _ = server.waitAndReturnRequests(count: 1) + } + + // MARK: - Interception Values + + func testGivenURLSessionWithDatadogDelegate_whenTaskCompletesWithFailure_itPassesAllValuesToTheInterceptor() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 + + let expectedError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "some error"]) + let server = ServerMock(delivery: .failure(error: expectedError)) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + let dateBeforeAnyRequests = Date() + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + let url1: URL = .mockRandom() + session + .dataTask(with: url1) + .resume() + + let url2: URL = .mockRandom() + session + .dataTask(with: URLRequest(url: url2)) { _,_,_ in } + .resume() + + // Then + _ = server.waitAndReturnRequests(count: 2) + + waitForExpectations(timeout: 5, handler: nil) + let dateAfterAllRequests = Date() + + XCTAssertEqual(handler.interceptions.count, 2, "Interceptor should record metrics for 2 tasks") + + try [url1, url2].forEach { url in + let interception = try XCTUnwrap(handler.interception(for: url)) + let metrics = try XCTUnwrap(interception.metrics) + XCTAssertGreaterThan(metrics.fetch.start, dateBeforeAnyRequests) + XCTAssertLessThan(metrics.fetch.end, dateAfterAllRequests) + XCTAssertNil(interception.data, "Data should not be recorded for \(url)") + XCTAssertEqual((interception.completion?.error as? NSError)?.localizedDescription, "some error") + } + } + + func testGivenURLSessionWithCustomDelegate_whenNotInstrumented_itDoesNotInterceptTasks() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: Data())) + + // Given + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession() // no custom delegate + + // When + let url1: URL = .mockRandom() + session + .dataTask(with: url1) + .resume() + + let url2: URL = .mockRandom() + session + .dataTask(with: URLRequest(url: url2)) + .resume() + + // Then + _ = server.waitAndReturnRequests(count: 2) + XCTAssertEqual(handler.interceptions.count, 0, "Interceptor should not record tasks") + } + + func testGivenURLSessionWithDatadogDelegate_whenTaskCompletesWithSuccess_itPassesAllValuesToTheInterceptor() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 + + let randomData: Data = .mockRandom() + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: randomData)) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + let dateBeforeAnyRequests = Date() + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + + // When + let url1 = URL.mockRandom() + session + .dataTask(with: url1) + .resume() + + let url2 = URL.mockRandom() + session + .dataTask(with: URLRequest(url: url2)) + .resume() + + // Then + _ = server.waitAndReturnRequests(count: 2) + + waitForExpectations(timeout: 5, handler: nil) + let dateAfterAllRequests = Date() + XCTAssertEqual(handler.interceptions.count, 2, "Interceptor should record metrics for 2 tasks") + + try [url1, url2].forEach { url in + let interception = try XCTUnwrap(handler.interception(for: url)) + let metrics = try XCTUnwrap(interception.metrics) + XCTAssertGreaterThan(metrics.fetch.start, dateBeforeAnyRequests) + XCTAssertLessThan(metrics.fetch.end, dateAfterAllRequests) + XCTAssertEqual(interception.data, randomData) + XCTAssertNotNil(interception.completion) + XCTAssertNil(interception.completion?.error) + } + } + + @available(iOS 13.0, tvOS 13.0, *) + func testGivenURLSessionWithCustomDelegate_whenUsingAsyncData_itPassesAllValuesToTheInterceptor() async throws { + /// Testing only 16.0 or above because 15.0 has ThreadSanitizer issues with async APIs + guard #available(iOS 16, tvOS 16, *) else { + return + } + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 + + let expectedError = NSError(domain: "network", code: 999, userInfo: [NSLocalizedDescriptionKey: "some error"]) + let server = ServerMock( + delivery: .failure(error: expectedError), + skipIsMainThreadCheck: true + ) + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + let dateBeforeAnyRequests = Date() + + // Given + let delegate = MockDelegate() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + let session = server.getInterceptedURLSession() + + // When + _ = try? await session.data(from: .mockRandom(), delegate: delegate) // intercepted + _ = try? await session.data(for: URLRequest(url: .mockRandom()), delegate: delegate) // intercepted + _ = try? await session.data(for: URLRequest(url: .mockRandom())) // not intercepted + + // Then + await dd_fulfillment( + for: [ + notifyInterceptionDidStart, + notifyInterceptionDidComplete + ], + timeout: 5, + enforceOrder: true + ) + + _ = server.waitAndReturnRequests(count: 3) + + let dateAfterAllRequests = Date() + + XCTAssertEqual(handler.interceptions.count, 2, "Interceptor should record metrics for 2 tasks") + + handler.interceptions.forEach { id, interception in + XCTAssertGreaterThan(interception.metrics?.fetch.start ?? .distantPast, dateBeforeAnyRequests) + XCTAssertLessThan(interception.metrics?.fetch.end ?? .distantFuture, dateAfterAllRequests) + XCTAssertNil(interception.data, "Data should not be recorded for \(id)") + XCTAssertEqual((interception.completion?.error as? NSError)?.localizedDescription, "some error") + } + } + + func testGivenURLSessionTask_withCustomDelegate_itInterceptsRequests() throws { + // pre iOS 15 cannot set delegate per task + guard #available(iOS 15, tvOS 15, *) else { + return + } + + let notifyInterceptionDidComplete = expectation(description: "Notify intercepion did complete") + notifyInterceptionDidComplete.expectedFulfillmentCount = 2 + handler.onInterceptionDidComplete = { _ in notifyInterceptionDidComplete.fulfill() } + + let server = ServerMock( + delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10)), + skipIsMainThreadCheck: true + ) + + // Given + let delegate1 = MockDelegate() + let delegate2 = MockDelegate2() + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self), in: core) + + let session = server.getInterceptedURLSession() + + // When + let task1 = session.dataTask(with: URL.mockWith(url: "https://www.foo.com/1")) // intercepted + task1.delegate = delegate1 + task1.resume() + + let task2 = session.dataTask(with: URL.mockWith(url: "https://www.foo.com/2")) // intercepted + task2.delegate = delegate1 + task2.resume() + + let task3 = session.dataTask(with: URL.mockWith(url: "https://www.foo.com/3")) // not intercepted + task3.delegate = delegate2 + task3.resume() + + // Then + _ = server.waitAndReturnRequests(count: 3) + waitForExpectations(timeout: 5, handler: nil) + XCTAssertEqual(handler.interceptions.count, 2, "Interceptor should intercept 2 tasks") + } + + // MARK: - Usage + + @available(*, deprecated) + func testItCanBeInitializedBeforeInitializingDefaultSDKCore() throws { + // Given + let delegate1 = DatadogURLSessionDelegate() + let delegate2 = DatadogURLSessionDelegate(additionalFirstPartyHosts: []) + let delegate3 = DatadogURLSessionDelegate(additionalFirstPartyHostsWithHeaderTypes: [:]) + + // When + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // Then + XCTAssertNotNil(delegate1.feature) + XCTAssertNotNil(delegate2.feature) + XCTAssertNotNil(delegate3.feature) + } + + @available(*, deprecated) + func testItCanBeInitializedAfterInitializingDefaultSDKCore() throws { + // Given + CoreRegistry.register(default: core) + defer { CoreRegistry.unregisterDefault() } + + // When + let delegate1 = DatadogURLSessionDelegate() + let delegate2 = DatadogURLSessionDelegate(additionalFirstPartyHosts: []) + let delegate3 = DatadogURLSessionDelegate(additionalFirstPartyHostsWithHeaderTypes: [:]) + + // Then + XCTAssertNotNil(delegate1.feature) + XCTAssertNotNil(delegate2.feature) + XCTAssertNotNil(delegate3.feature) + } + + @available(*, deprecated) + func testItOnlyKeepsInstrumentationWhileSDKCoreIsAvailableInMemory() throws { + // Given + let delegate = DatadogURLSessionDelegate(in: core) + // Then + XCTAssertNotNil(delegate.feature) + + // When (deinitialize core) + core = nil + // Then + XCTAssertNil(delegate.feature) + } + + func testWhenEnableInstrumentationOnTheSameDelegate_thenItPrintsAWarning() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + URLSessionInstrumentation.enable(with: .init(delegateClass: MockDelegate.self), in: core) + URLSessionInstrumentation.enable(with: .init(delegateClass: MockDelegate.self), in: core) + + // Then + XCTAssertEqual( + dd.logger.warnLog?.message, + """ + The delegate class MockDelegate is already instrumented. + The previous instrumentation will be disabled in favor of the new one. + """ + ) + } + + // MARK: - URLSessionTask Interception + + func testWhenInterceptingTaskWithMultipleTraceContexts_itTakesTheFirstContext() throws { + let traceContexts = [ + TraceContext(traceID: .mock(1, 1), spanID: .mock(2), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(2, 2), spanID: .mock(3), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(3, 3), spanID: .mock(4), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + ] + + // When + let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) + feature.intercept(task: .mockAny(), with: traceContexts, additionalFirstPartyHosts: nil) + feature.flush() + + // Then + let interception = try XCTUnwrap(handler.interceptions.first?.value) + XCTAssertEqual(interception.trace, traceContexts.first, "It should register first injected Trace Context") + } + + // MARK: - First Party Hosts + + func testGivenHandler_whenInterceptingRequests_itDetectFirstPartyHost() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + + // Given + let delegate = MockDelegate() + let firstPartyHosts: URLSessionInstrumentation.FirstPartyHostsTracing = .traceWithHeaders(hostsWithHeaders: ["test.com": [.datadog]]) + try URLSessionInstrumentation.enableOrThrow(with: .init(delegateClass: MockDelegate.self, firstPartyHostsTracing: firstPartyHosts), in: core) + + let session = server.getInterceptedURLSession(delegate: delegate) + let request: URLRequest = .mockWith(url: "https://test.com") + + handler.onInterceptionDidStart = { + // Then + XCTAssertTrue($0.isFirstPartyRequest) + notifyInterceptionDidStart.fulfill() + } + + // When + session + .dataTask(with: request) + .resume() + + // Then + waitForExpectations(timeout: 5, handler: nil) + _ = server.waitAndReturnRequests(count: 1) + } + + @available(*, deprecated) + func testGivenDelegateSubclass_whenInterceptingRequests_itDetectFirstPartyHost() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + + // Given + + let delegate = DatadogURLSessionDelegate( + in: core, + additionalFirstPartyHostsWithHeaderTypes: ["test.com": [.datadog]] + ) + + let session = server.getInterceptedURLSession(delegate: delegate) + let request: URLRequest = .mockWith(url: "https://test.com") + + handler.onInterceptionDidStart = { + // Then + XCTAssertTrue($0.isFirstPartyRequest) + notifyInterceptionDidStart.fulfill() + } + + // When + session + .dataTask(with: request) + .resume() + + session + .dataTask(with: request) { _,_,_ in } + .resume() + + // Then + waitForExpectations(timeout: 5, handler: nil) + _ = server.waitAndReturnRequests(count: 2) + + // release the delegate to unswizzle + session.finishTasksAndInvalidate() + } + + @available(*, deprecated) + func testGivenCompositeDelegate_whenInterceptingRequests_itDetectFirstPartyHost() throws { + let notifyInterceptionDidStart = expectation(description: "Notify interception did start") + notifyInterceptionDidStart.expectedFulfillmentCount = 2 + + handler.onInterceptionDidStart = { _ in notifyInterceptionDidStart.fulfill() } + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200), data: .mock(ofSize: 10))) + + // Given + class CompositeDelegate: NSObject, URLSessionDataDelegate, __URLSessionDelegateProviding { + let ddURLSessionDelegate: DatadogURLSessionDelegate + + required init(in core: DatadogCoreProtocol) { + ddURLSessionDelegate = DatadogURLSessionDelegate( + in: core, + additionalFirstPartyHostsWithHeaderTypes: ["test.com": [.datadog, .tracecontext]] + ) + + super.init() + } + } + + let delegate = CompositeDelegate(in: core) + let session = server.getInterceptedURLSession(delegate: delegate) + let request: URLRequest = .mockWith(url: "https://test.com") + + handler.onInterceptionDidStart = { + // Then + XCTAssertTrue($0.isFirstPartyRequest) + notifyInterceptionDidStart.fulfill() + } + + // When + session + .dataTask(with: request) + .resume() + + session + .dataTask(with: request) { _,_,_ in } + .resume() + + // Then + waitForExpectations(timeout: 5, handler: nil) + _ = server.waitAndReturnRequests(count: 2) + + // release the delegate to unswizzle + session.finishTasksAndInvalidate() + } + + // MARK: - Thread Safety + + func testRandomlyCallingDifferentAPIsConcurrentlyDoesNotCrash() throws { + let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) + + let requests = [ + URLRequest(url: URL(string: "https://api.first-party.com/v1/endpoint")!), + URLRequest(url: URL(string: "https://api.third-party.com/v1/endpoint")!), + URLRequest(url: URL(string: "https://dd.internal.com/v1/endpoint")!) + ] + let tasks = (0..<10).map { _ in URLSessionTask.mockWith(request: .mockAny(), response: .mockAny()) } + + // swiftlint:disable opening_brace trailing_closure + callConcurrently( + closures: [ + { feature.handlers = [self.handler] }, + { _ = feature.intercept(request: requests.randomElement()!, additionalFirstPartyHosts: nil) }, + { feature.intercept(task: tasks.randomElement()!, with: [], additionalFirstPartyHosts: nil) }, + { feature.task(tasks.randomElement()!, didReceive: .mockRandom()) }, + { feature.task(tasks.randomElement()!, didFinishCollecting: .mockAny()) }, + { feature.task(tasks.randomElement()!, didCompleteWithError: nil) }, + { try? feature.bind(configuration: .init(delegateClass: MockDelegate.self)) }, + { feature.unbind(delegateClass: MockDelegate.self) } + ], + iterations: 50 + ) + // swiftlint:enable opening_brace trailing_closure + } + + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + class MockDelegate2: NSObject, URLSessionDataDelegate { + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/SpanIDGeneratorTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/SpanIDGeneratorTests.swift new file mode 100644 index 0000000000..e153d90f6b --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/SpanIDGeneratorTests.swift @@ -0,0 +1,27 @@ +/* + * 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 SpanIDGeneratorTests: XCTestCase { + func testDefaultGenerationBoundaries() { + let generator = DefaultSpanIDGenerator() + XCTAssertEqual(generator.range.lowerBound, 1) + XCTAssertEqual(generator.range.upperBound, 9_223_372_036_854_775_807) // 2 ^ 63 -1 + } + + func testItGeneratesUUIDsFromGivenBoundaries() { + let generator = DefaultSpanIDGenerator(range: 10...15) + var generatedUUIDs: Set = [] + + (0..<1_000).forEach { _ in + generatedUUIDs.insert(generator.generate()) + } + + XCTAssertEqual(generatedUUIDs, [10, 11, 12, 13, 14, 15]) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/SpanIDTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/SpanIDTests.swift new file mode 100644 index 0000000000..43db021c64 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/SpanIDTests.swift @@ -0,0 +1,139 @@ +/* + * 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 + +class SpanIDTests: XCTestCase { + func testToHexadecimalStringConversion() { + XCTAssertEqual(String(SpanID(rawValue: 0), representation: .hexadecimal), "0") + XCTAssertEqual(String(SpanID(rawValue: 1), representation: .hexadecimal), "1") + XCTAssertEqual(String(SpanID(rawValue: 15), representation: .hexadecimal), "f") + XCTAssertEqual(String(SpanID(rawValue: 16), representation: .hexadecimal), "10") + XCTAssertEqual(String(SpanID(rawValue: 123), representation: .hexadecimal), "7b") + XCTAssertEqual(String(SpanID(rawValue: 123_456), representation: .hexadecimal), "1e240") + XCTAssertEqual(String(SpanID(rawValue: .max), representation: .hexadecimal), "ffffffffffffffff") + } + + func testTo16CharHexadecimalStringConversion() { + XCTAssertEqual(String(SpanID(rawValue: 0), representation: .hexadecimal16Chars), "0000000000000000") + XCTAssertEqual(String(SpanID(rawValue: 1), representation: .hexadecimal16Chars), "0000000000000001") + XCTAssertEqual(String(SpanID(rawValue: 15), representation: .hexadecimal16Chars), "000000000000000f") + XCTAssertEqual(String(SpanID(rawValue: 16), representation: .hexadecimal16Chars), "0000000000000010") + XCTAssertEqual(String(SpanID(rawValue: 123), representation: .hexadecimal16Chars), "000000000000007b") + XCTAssertEqual(String(SpanID(rawValue: 123_456), representation: .hexadecimal16Chars), "000000000001e240") + XCTAssertEqual(String(SpanID(rawValue: .max), representation: .hexadecimal16Chars), "ffffffffffffffff") + } + + func testTo32CharHexadecimalStringConversion() { + XCTAssertEqual(String(SpanID(rawValue: 0), representation: .hexadecimal32Chars), "00000000000000000000000000000000") + XCTAssertEqual(String(SpanID(rawValue: 1), representation: .hexadecimal32Chars), "00000000000000000000000000000001") + XCTAssertEqual(String(SpanID(rawValue: 15), representation: .hexadecimal32Chars), "0000000000000000000000000000000f") + XCTAssertEqual(String(SpanID(rawValue: 16), representation: .hexadecimal32Chars), "00000000000000000000000000000010") + XCTAssertEqual(String(SpanID(rawValue: 123), representation: .hexadecimal32Chars), "0000000000000000000000000000007b") + XCTAssertEqual(String(SpanID(rawValue: 123_456), representation: .hexadecimal32Chars), "0000000000000000000000000001e240") + XCTAssertEqual(String(SpanID(rawValue: .max), representation: .hexadecimal32Chars), "0000000000000000ffffffffffffffff") + } + + func testToDecimalStringConversion() { + XCTAssertEqual(String(SpanID(rawValue: 0)), "0") + XCTAssertEqual(String(SpanID(rawValue: 1)), "1") + XCTAssertEqual(String(SpanID(rawValue: 15)), "15") + XCTAssertEqual(String(SpanID(rawValue: 16)), "16") + XCTAssertEqual(String(SpanID(rawValue: 123)), "123") + XCTAssertEqual(String(SpanID(rawValue: 123_456)), "123456") + XCTAssertEqual(String(SpanID(rawValue: .max)), "\(UInt64.max)") + } + + func testInitializationFromHexadecimal() { + XCTAssertEqual(SpanID("0", representation: .hexadecimal), 0) + XCTAssertEqual(SpanID("1", representation: .hexadecimal), 1) + XCTAssertEqual(SpanID("f", representation: .hexadecimal), 15) + XCTAssertEqual(SpanID("10", representation: .hexadecimal), 16) + XCTAssertEqual(SpanID("7b", representation: .hexadecimal), 123) + XCTAssertEqual(SpanID("1e240", representation: .hexadecimal), 123_456) + XCTAssertEqual(SpanID("FFFFFFFFFFFFFFFF", representation: .hexadecimal), SpanID(rawValue: .max)) + } + + func testInitializationFromDecimal() { + XCTAssertEqual(String(SpanID("0")!, representation: .hexadecimal), "0") + XCTAssertEqual(String(SpanID("1")!, representation: .hexadecimal), "1") + XCTAssertEqual(String(SpanID("15")!, representation: .hexadecimal), "f") + XCTAssertEqual(String(SpanID("16")!, representation: .hexadecimal), "10") + XCTAssertEqual(String(SpanID("123")!, representation: .hexadecimal), "7b") + XCTAssertEqual(String(SpanID("123456")!, representation: .hexadecimal), "1e240") + XCTAssertEqual(String(SpanID("\(UInt64.max)")!, representation: .hexadecimal), "ffffffffffffffff") + } + + func testEncodableFromDecimal() { + let json = "1234" + let decoder = JSONDecoder() + let spanID = try! decoder.decode(SpanID.self, from: json.data(using: .utf8)!) + XCTAssertEqual(spanID, SpanID(rawValue: 1_234)) + } + + func testEncodableFromString() { + let json = "\"1234\"" + let decoder = JSONDecoder() + let spanID = try! decoder.decode(SpanID.self, from: json.data(using: .utf8)!) + XCTAssertEqual(spanID, SpanID(rawValue: 1_234)) + } + + func testDecodableUnknownFormat() { + let json = "1f" + let decoder = JSONDecoder() + XCTAssertThrowsError(try decoder.decode(SpanID.self, from: json.data(using: .utf8)!) as SpanID) + } + + func testDecodable() { + let spanID = SpanID(rawValue: 1_234) + let encoder = JSONEncoder() + let json = try! encoder.encode(spanID) + XCTAssertEqual(String(data: json, encoding: .utf8), "1234") + } + + func testToString() { + // hexadecimal + XCTAssertEqual(SpanID(rawValue: 0).toString(representation: .hexadecimal), "0") + XCTAssertEqual(SpanID(rawValue: 1).toString(representation: .hexadecimal), "1") + XCTAssertEqual(SpanID(rawValue: 15).toString(representation: .hexadecimal), "f") + XCTAssertEqual(SpanID(rawValue: 16).toString(representation: .hexadecimal), "10") + XCTAssertEqual(SpanID(rawValue: 123).toString(representation: .hexadecimal), "7b") + XCTAssertEqual(SpanID(rawValue: 123_456).toString(representation: .hexadecimal), "1e240") + XCTAssertEqual(SpanID(rawValue: .max).toString(representation: .hexadecimal), "ffffffffffffffff") + + // hexadecimal16Chars + XCTAssertEqual(SpanID(rawValue: 0).toString(representation: .hexadecimal16Chars), "0000000000000000") + XCTAssertEqual(SpanID(rawValue: 1).toString(representation: .hexadecimal16Chars), "0000000000000001") + XCTAssertEqual(SpanID(rawValue: 15).toString(representation: .hexadecimal16Chars), "000000000000000f") + XCTAssertEqual(SpanID(rawValue: 16).toString(representation: .hexadecimal16Chars), "0000000000000010") + XCTAssertEqual(SpanID(rawValue: 123).toString(representation: .hexadecimal16Chars), "000000000000007b") + XCTAssertEqual(SpanID(rawValue: 123_456).toString(representation: .hexadecimal16Chars), "000000000001e240") + XCTAssertEqual(SpanID(rawValue: .max).toString(representation: .hexadecimal16Chars), "ffffffffffffffff") + + // hexadecimal32Chars + XCTAssertEqual(SpanID(rawValue: 0).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000000") + XCTAssertEqual(SpanID(rawValue: 1).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000001") + XCTAssertEqual(SpanID(rawValue: 15).toString(representation: .hexadecimal32Chars), "0000000000000000000000000000000f") + XCTAssertEqual(SpanID(rawValue: 16).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000010") + XCTAssertEqual(SpanID(rawValue: 123).toString(representation: .hexadecimal32Chars), "0000000000000000000000000000007b") + XCTAssertEqual(SpanID(rawValue: 123_456).toString(representation: .hexadecimal32Chars), "0000000000000000000000000001e240") + XCTAssertEqual(SpanID(rawValue: .max).toString(representation: .hexadecimal32Chars), "0000000000000000ffffffffffffffff") + + // decimal + XCTAssertEqual(SpanID(rawValue: 0).toString(representation: .decimal), "0") + XCTAssertEqual(SpanID(rawValue: 1).toString(representation: .decimal), "1") + XCTAssertEqual(SpanID(rawValue: 15).toString(representation: .decimal), "15") + XCTAssertEqual(SpanID(rawValue: 16).toString(representation: .decimal), "16") + XCTAssertEqual(SpanID(rawValue: 123).toString(representation: .decimal), "123") + XCTAssertEqual(SpanID(rawValue: 123_456).toString(representation: .decimal), "123456") + XCTAssertEqual(SpanID(rawValue: .max).toString(representation: .decimal), "\(UInt64.max)") + } + + func testDefaultInit() { + XCTAssertEqual(SpanID(), 0) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/TraceIDGeneratorTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/TraceIDGeneratorTests.swift new file mode 100644 index 0000000000..da73e78050 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/TraceIDGeneratorTests.swift @@ -0,0 +1,39 @@ +/* + * 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 TraceIDGeneratorTests: XCTestCase { + func testDefaultGenerationBoundaries() { + let generator = DefaultTraceIDGenerator() + XCTAssertEqual(generator.range.lowerBound, 1) + XCTAssertEqual(generator.range.upperBound, 18_446_744_073_709_551_615) + } + + func testItGeneratesUUIDsFromGivenBoundaries() { + let generator = DefaultTraceIDGenerator(range: 10...15) + + let lowerBound = UInt32(Date().timeIntervalSince1970) + (0..<1_000).forEach { _ in + let id = generator.generate() + let upperBound = UInt32(Date().timeIntervalSince1970) + + XCTAssertGreaterThanOrEqual(id.idLo, 10) + XCTAssertLessThanOrEqual(id.idLo, 15) + + let idHiStr = String(id.idHi, radix: 10) + let idHi = UInt64(idHiStr) ?? 0 + + let seconds = UInt32(idHi >> 32) + XCTAssertGreaterThanOrEqual(seconds, lowerBound) + XCTAssertLessThanOrEqual(seconds, upperBound) + + let zeros = UInt32(idHi & 0xFFFFFFFF) + XCTAssertEqual(zeros, 0) + } + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/TraceIDTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/TraceIDTests.swift new file mode 100644 index 0000000000..20224ea670 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/TraceIDTests.swift @@ -0,0 +1,201 @@ +/* + * 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 + +class TraceIDTests: XCTestCase { + func testToHexadecimalStringConversion() { + // 64 bit or less + XCTAssertEqual(String(TraceID(rawValue: (0, 0)), representation: .hexadecimal), "0") + XCTAssertEqual(String(TraceID(rawValue: (0, 1)), representation: .hexadecimal), "1") + XCTAssertEqual(String(TraceID(rawValue: (0, 15)), representation: .hexadecimal), "f") + XCTAssertEqual(String(TraceID(rawValue: (0, 16)), representation: .hexadecimal), "10") + XCTAssertEqual(String(TraceID(rawValue: (0, 123)), representation: .hexadecimal), "7b") + XCTAssertEqual(String(TraceID(rawValue: (0, 123_456)), representation: .hexadecimal), "1e240") + XCTAssertEqual(String(TraceID(rawValue: (0, .max)), representation: .hexadecimal), "ffffffffffffffff") + + // 128 bit + XCTAssertEqual(String(TraceID(rawValue: (1, 0)), representation: .hexadecimal), "10000000000000000") + XCTAssertEqual(String(TraceID(rawValue: (1, 1)), representation: .hexadecimal), "10000000000000001") + XCTAssertEqual(String(TraceID(rawValue: (15, 15)), representation: .hexadecimal), "f000000000000000f") + XCTAssertEqual(String(TraceID(rawValue: (16, 16)), representation: .hexadecimal), "100000000000000010") + XCTAssertEqual(String(TraceID(rawValue: (123, 123)), representation: .hexadecimal), "7b000000000000007b") + XCTAssertEqual(String(TraceID(rawValue: (123_456, 123_456)), representation: .hexadecimal), "1e240000000000001e240") + XCTAssertEqual(String(TraceID(rawValue: (.max, .max)), representation: .hexadecimal), "ffffffffffffffffffffffffffffffff") + } + + func testTo16CharHexadecimalStringConversion() { + XCTAssertEqual(String(TraceID(rawValue: (0, 0)), representation: .hexadecimal16Chars), "0000000000000000") + XCTAssertEqual(String(TraceID(rawValue: (0, 1)), representation: .hexadecimal16Chars), "0000000000000001") + XCTAssertEqual(String(TraceID(rawValue: (0, 15)), representation: .hexadecimal16Chars), "000000000000000f") + XCTAssertEqual(String(TraceID(rawValue: (0, 16)), representation: .hexadecimal16Chars), "0000000000000010") + XCTAssertEqual(String(TraceID(rawValue: (0, 123)), representation: .hexadecimal16Chars), "000000000000007b") + XCTAssertEqual(String(TraceID(rawValue: (0, 123_456)), representation: .hexadecimal16Chars), "000000000001e240") + XCTAssertEqual(String(TraceID(rawValue: (0, .max)), representation: .hexadecimal16Chars), "ffffffffffffffff") + } + + func testTo32CharHexadecimalStringConversion() { + // 64 bit + XCTAssertEqual(String(TraceID(rawValue: (0, 0)), representation: .hexadecimal32Chars), "00000000000000000000000000000000") + XCTAssertEqual(String(TraceID(rawValue: (0, 1)), representation: .hexadecimal32Chars), "00000000000000000000000000000001") + XCTAssertEqual(String(TraceID(rawValue: (0, 15)), representation: .hexadecimal32Chars), "0000000000000000000000000000000f") + XCTAssertEqual(String(TraceID(rawValue: (0, 16)), representation: .hexadecimal32Chars), "00000000000000000000000000000010") + XCTAssertEqual(String(TraceID(rawValue: (0, 123)), representation: .hexadecimal32Chars), "0000000000000000000000000000007b") + XCTAssertEqual(String(TraceID(rawValue: (0, 123_456)), representation: .hexadecimal32Chars), "0000000000000000000000000001e240") + XCTAssertEqual(String(TraceID(rawValue: (0, .max)), representation: .hexadecimal32Chars), "0000000000000000ffffffffffffffff") + + // 128 bit + XCTAssertEqual(String(TraceID(rawValue: (1, 0)), representation: .hexadecimal32Chars), "00000000000000010000000000000000") + XCTAssertEqual(String(TraceID(rawValue: (1, 1)), representation: .hexadecimal32Chars), "00000000000000010000000000000001") + XCTAssertEqual(String(TraceID(rawValue: (15, 15)), representation: .hexadecimal32Chars), "000000000000000f000000000000000f") + XCTAssertEqual(String(TraceID(rawValue: (16, 16)), representation: .hexadecimal32Chars), "00000000000000100000000000000010") + XCTAssertEqual(String(TraceID(rawValue: (123, 123)), representation: .hexadecimal32Chars), "000000000000007b000000000000007b") + XCTAssertEqual(String(TraceID(rawValue: (123_456, 123_456)), representation: .hexadecimal32Chars), "000000000001e240000000000001e240") + XCTAssertEqual(String(TraceID(rawValue: (.max, .max)), representation: .hexadecimal32Chars), "ffffffffffffffffffffffffffffffff") + } + + func testToDecimalStringConversion() { + XCTAssertEqual(String(TraceID(rawValue: (0, 0)), representation: .decimal), "0") + XCTAssertEqual(String(TraceID(rawValue: (0, 1)), representation: .decimal), "1") + XCTAssertEqual(String(TraceID(rawValue: (0, 15)), representation: .decimal), "15") + XCTAssertEqual(String(TraceID(rawValue: (0, 16)), representation: .decimal), "16") + XCTAssertEqual(String(TraceID(rawValue: (0, 123)), representation: .decimal), "123") + XCTAssertEqual(String(TraceID(rawValue: (0, 123_456)), representation: .decimal), "123456") + XCTAssertEqual(String(TraceID(rawValue: (0, .max)), representation: .decimal), "\(UInt64.max)") + } + + func testInitializationFromHexadecimal() { + // 64 bit or less + XCTAssertEqual(TraceID("0", representation: .hexadecimal), 0) + XCTAssertEqual(TraceID("1", representation: .hexadecimal), 1) + XCTAssertEqual(TraceID("f", representation: .hexadecimal), 15) + XCTAssertEqual(TraceID("10", representation: .hexadecimal), 16) + XCTAssertEqual(TraceID("7b", representation: .hexadecimal), 123) + XCTAssertEqual(TraceID("1e240", representation: .hexadecimal), 123_456) + XCTAssertEqual(TraceID("FFFFFFFFFFFFFFFF", representation: .hexadecimal), TraceID(rawValue: (0, .max))) + + // 128 bit + XCTAssertEqual(TraceID("10000000000000000", representation: .hexadecimal), TraceID(rawValue: (1, 0))) + XCTAssertEqual(TraceID("10000000000000001", representation: .hexadecimal), TraceID(rawValue: (1, 1))) + XCTAssertEqual(TraceID("f000000000000000f", representation: .hexadecimal), TraceID(rawValue: (15, 15))) + XCTAssertEqual(TraceID("100000000000000010", representation: .hexadecimal), TraceID(rawValue: (16, 16))) + XCTAssertEqual(TraceID("7b000000000000007b", representation: .hexadecimal), TraceID(rawValue: (123, 123))) + XCTAssertEqual(TraceID("1e240000000000001e240", representation: .hexadecimal), TraceID(rawValue: (123_456, 123_456))) + XCTAssertEqual(TraceID("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", representation: .hexadecimal), TraceID(rawValue: (.max, .max))) + + // invalid + XCTAssertNil(TraceID("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFG", representation: .hexadecimal)) + XCTAssertNil(TraceID("-1", representation: .hexadecimal)) + XCTAssertNil(TraceID("FFFFFFFFFFFFFFFFG", representation: .hexadecimal)) + XCTAssertNil(TraceID("1FFFFFFFFFFFFFFFF", representation: .hexadecimal16Chars)) + XCTAssertNil(TraceID("1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", representation: .hexadecimal32Chars)) + } + + func testInitializationFromDecimal() { + XCTAssertEqual(String(TraceID("0")!, representation: .hexadecimal), "0") + XCTAssertEqual(String(TraceID("1")!, representation: .hexadecimal), "1") + XCTAssertEqual(String(TraceID("15")!, representation: .hexadecimal), "f") + XCTAssertEqual(String(TraceID("16")!, representation: .hexadecimal), "10") + XCTAssertEqual(String(TraceID("123")!, representation: .hexadecimal), "7b") + XCTAssertEqual(String(TraceID("123456")!, representation: .hexadecimal), "1e240") + XCTAssertEqual(String(TraceID("\(UInt64.max)")!, representation: .hexadecimal), "ffffffffffffffff") + + // invalid + XCTAssertNil(TraceID("-1")) + XCTAssertNil(TraceID("\(UInt64.max)0")) + } + + func testDecodableFromHexadecimal() { + let json = "\"1e240\"" + let decoder = JSONDecoder() + let traceID = try! decoder.decode(TraceID.self, from: json.data(using: .utf8)!) + XCTAssertEqual(traceID, TraceID(rawValue: (0, 123_456))) + } + + func testDecodableUnknownFormat() { + let json = "1234" + let decoder = JSONDecoder() + XCTAssertThrowsError(try decoder.decode(TraceID.self, from: json.data(using: .utf8)!) as TraceID) + } + + func testEncodableToHexadecimal() { + let traceID = TraceID(rawValue: (0, 123_456)) + let encoder = JSONEncoder() + let json = try! encoder.encode(traceID) + XCTAssertEqual(String(data: json, encoding: .utf8), "\"1e240\"") + } + + func testIdHiHex() { + XCTAssertEqual(TraceID(rawValue: (0, 0)).idHiHex, "0") + XCTAssertEqual(TraceID(rawValue: (1, 0)).idHiHex, "1") + XCTAssertEqual(TraceID(rawValue: (15, 0)).idHiHex, "f") + XCTAssertEqual(TraceID(rawValue: (16, 0)).idHiHex, "10") + XCTAssertEqual(TraceID(rawValue: (123, 0)).idHiHex, "7b") + XCTAssertEqual(TraceID(rawValue: (123_456, 0)).idHiHex, "1e240") + XCTAssertEqual(TraceID(rawValue: (.max, 0)).idHiHex, "ffffffffffffffff") + } + + func testIdLoHex() { + XCTAssertEqual(TraceID(rawValue: (0, 0)).idLoHex, "0") + XCTAssertEqual(TraceID(rawValue: (0, 1)).idLoHex, "1") + XCTAssertEqual(TraceID(rawValue: (0, 15)).idLoHex, "f") + XCTAssertEqual(TraceID(rawValue: (0, 16)).idLoHex, "10") + XCTAssertEqual(TraceID(rawValue: (0, 123)).idLoHex, "7b") + XCTAssertEqual(TraceID(rawValue: (0, 123_456)).idLoHex, "1e240") + XCTAssertEqual(TraceID(rawValue: (0, .max)).idLoHex, "ffffffffffffffff") + } + + func testToString() { + // hexadecimal + XCTAssertEqual(TraceID(rawValue: (0, 0)).toString(representation: .hexadecimal), "0") + XCTAssertEqual(TraceID(rawValue: (0, 1)).toString(representation: .hexadecimal), "1") + XCTAssertEqual(TraceID(rawValue: (0, 15)).toString(representation: .hexadecimal), "f") + XCTAssertEqual(TraceID(rawValue: (0, 16)).toString(representation: .hexadecimal), "10") + XCTAssertEqual(TraceID(rawValue: (0, 123)).toString(representation: .hexadecimal), "7b") + XCTAssertEqual(TraceID(rawValue: (0, 123_456)).toString(representation: .hexadecimal), "1e240") + XCTAssertEqual(TraceID(rawValue: (0, .max)).toString(representation: .hexadecimal), "ffffffffffffffff") + XCTAssertEqual(TraceID(rawValue: (1, .max)).toString(representation: .hexadecimal), "1ffffffffffffffff") + XCTAssertEqual(TraceID(rawValue: (.max, .max)).toString(representation: .hexadecimal), "ffffffffffffffffffffffffffffffff") + + // hexadecimal16Chars + XCTAssertEqual(TraceID(rawValue: (0, 0)).toString(representation: .hexadecimal16Chars), "0000000000000000") + XCTAssertEqual(TraceID(rawValue: (0, 1)).toString(representation: .hexadecimal16Chars), "0000000000000001") + XCTAssertEqual(TraceID(rawValue: (0, 15)).toString(representation: .hexadecimal16Chars), "000000000000000f") + XCTAssertEqual(TraceID(rawValue: (0, 16)).toString(representation: .hexadecimal16Chars), "0000000000000010") + XCTAssertEqual(TraceID(rawValue: (0, 123)).toString(representation: .hexadecimal16Chars), "000000000000007b") + XCTAssertEqual(TraceID(rawValue: (0, 123_456)).toString(representation: .hexadecimal16Chars), "000000000001e240") + XCTAssertEqual(TraceID(rawValue: (0, .max)).toString(representation: .hexadecimal16Chars), "ffffffffffffffff") + XCTAssertEqual(TraceID(rawValue: (1, .max)).toString(representation: .hexadecimal16Chars), "ffffffffffffffff") + XCTAssertEqual(TraceID(rawValue: (.max, .max)).toString(representation: .hexadecimal16Chars), "ffffffffffffffff") + + // hexadecimal32Chars + XCTAssertEqual(TraceID(rawValue: (0, 0)).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000000") + XCTAssertEqual(TraceID(rawValue: (0, 1)).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000001") + XCTAssertEqual(TraceID(rawValue: (0, 15)).toString(representation: .hexadecimal32Chars), "0000000000000000000000000000000f") + XCTAssertEqual(TraceID(rawValue: (0, 16)).toString(representation: .hexadecimal32Chars), "00000000000000000000000000000010") + XCTAssertEqual(TraceID(rawValue: (0, 123)).toString(representation: .hexadecimal32Chars), "0000000000000000000000000000007b") + XCTAssertEqual(TraceID(rawValue: (0, 123_456)).toString(representation: .hexadecimal32Chars), "0000000000000000000000000001e240") + XCTAssertEqual(TraceID(rawValue: (0, .max)).toString(representation: .hexadecimal32Chars), "0000000000000000ffffffffffffffff") + XCTAssertEqual(TraceID(rawValue: (1, .max)).toString(representation: .hexadecimal32Chars), "0000000000000001ffffffffffffffff") + + // decimal + XCTAssertEqual(TraceID(rawValue: (0, 0)).toString(representation: .decimal), "0") + XCTAssertEqual(TraceID(rawValue: (0, 1)).toString(representation: .decimal), "1") + XCTAssertEqual(TraceID(rawValue: (0, 15)).toString(representation: .decimal), "15") + XCTAssertEqual(TraceID(rawValue: (0, 16)).toString(representation: .decimal), "16") + XCTAssertEqual(TraceID(rawValue: (0, 123)).toString(representation: .decimal), "123") + XCTAssertEqual(TraceID(rawValue: (0, 123_456)).toString(representation: .decimal), "123456") + XCTAssertEqual(TraceID(rawValue: (0, .max)).toString(representation: .decimal), "\(UInt64.max)") + XCTAssertEqual(TraceID(rawValue: (1, .max)).toString(representation: .decimal), "\(UInt64.max)") + XCTAssertEqual(TraceID(rawValue: (.max, .max)).toString(representation: .decimal), "\(UInt64.max)") + } + + func testDefaultInit() { + XCTAssertEqual(TraceID().rawValue.0, 0) + XCTAssertEqual(TraceID().rawValue.1, 0) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift new file mode 100644 index 0000000000..fa2fea5338 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDataDelegateSwizzlerTests.swift @@ -0,0 +1,100 @@ +/* + * 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 URLSessionDataDelegateSwizzlerTests: XCTestCase { + func testSwizzling_implementedMethods() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { } + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { } + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { } + } + + let delegate = MockDelegate() + let didReceiveData = expectation(description: "didReceiveData") + didReceiveData.assertForOverFulfill = false + + // Given + let swizzler = URLSessionDataDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidReceive: { _, _, _ in + didReceiveData.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // intercepted + + wait(for: [didReceiveData], timeout: 5) + } + + func testSwizzling_whenMethodsNotImplemented() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + let delegate = MockDelegate() + let didReceiveData = expectation(description: "didReceiveData") + didReceiveData.assertForOverFulfill = false + + // Given + let swizzler = URLSessionDataDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidReceive: { _, _, _ in + didReceiveData.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // intercepted + + wait(for: [didReceiveData], timeout: 5) + } + + func testUnSwizzling() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + let delegate = MockDelegate() + let expectation = self.expectation(description: "not expected") + expectation.isInverted = true + + // Given + let swizzler = URLSessionDataDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidReceive: { _, _, _ in + expectation.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // not intercepted + + swizzler.unswizzle() + + waitForExpectations(timeout: 5) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDelegateAsSuperclassTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDelegateAsSuperclassTests.swift new file mode 100644 index 0000000000..1d560fab4d --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionDelegateAsSuperclassTests.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 +import DatadogInternal + +@available(*, deprecated) +internal final class _DatadogURLSessionDelegate: DatadogURLSessionDelegate { + let property: String + override init() { + property = "someProp" + super.init() + } + override func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + super.urlSession(session, task: task, didCompleteWithError: error) + } +} + +@available(*, deprecated) +class DDURLSessionDelegateAsSuperclassTests: XCTestCase { + func testSubclassability() { + // Success: tests compile, failure: compilation error + _ = _DatadogURLSessionDelegate() + } +} 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/NetworkInstrumentation/URLSessionSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift new file mode 100644 index 0000000000..cfb515f7ae --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift @@ -0,0 +1,40 @@ +/* + * 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 URLSessionSwizzlerTests: XCTestCase { + func testSwizzling_dataTaskWithCompletion() throws { + let didReceive = expectation(description: "didReceive") + didReceive.expectedFulfillmentCount = 2 + + let didInterceptCompletion = expectation(description: "interceptCompletion") + didInterceptCompletion.expectedFulfillmentCount = 2 + + let swizzler = URLSessionSwizzler() + + try swizzler.swizzle( + interceptCompletionHandler: { _, _, _ in + didInterceptCompletion.fulfill() + }, didReceive: { _, _ in + didReceive.fulfill() + } + ) + + let session = URLSession(configuration: .default) + let url = URL(string: "https://www.datadoghq.com/")! + session.dataTask(with: url) { _, _, _ in }.resume() // intercepted + session.dataTask(with: URLRequest(url: url)) { _, _, _ in }.resume() // intercepted + + swizzler.unswizzle() + session.dataTask(with: url) { _, _, _ in }.resume() // not intercepted + session.dataTask(with: URLRequest(url: url)) { _, _, _ in }.resume() // not intercepted + + wait(for: [didReceive, didInterceptCompletion], timeout: 5) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift new file mode 100644 index 0000000000..69839bd4bc --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskDelegateSwizzlerTests.swift @@ -0,0 +1,108 @@ +/* + * 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 URLSessionTaskDelegateSwizzlerTests: XCTestCase { + func testSwizzling_implementedMethods() throws { + class MockDelegate: NSObject, URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { } + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { } + } + + let delegate = MockDelegate() + let didFinishCollecting = expectation(description: "didFinishCollecting") + let didCompleteWithError = expectation(description: "didCompleteWithError") + + // Given + let swizzler = URLSessionTaskDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidFinishCollecting: { _, _, _ in + didFinishCollecting.fulfill() + }, + interceptDidCompleteWithError: { _, _, _ in + didCompleteWithError.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // intercepted + + wait(for: [didFinishCollecting, didCompleteWithError], timeout: 5) + } + + func testSwizzling_whenMethodsNotImplemented() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + let delegate = MockDelegate() + let didFinishCollecting = expectation(description: "didFinishCollecting") + let didCompleteWithError = expectation(description: "didCompleteWithError") + + // Given + let swizzler = URLSessionTaskDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidFinishCollecting: { _, _, _ in + didFinishCollecting.fulfill() + }, + interceptDidCompleteWithError: { _, _, _ in + didCompleteWithError.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // intercepted + + wait(for: [didFinishCollecting, didCompleteWithError], timeout: 5) + } + + func testUnSwizzling() throws { + class MockDelegate: NSObject, URLSessionDataDelegate { + } + + let delegate = MockDelegate() + let expectation = self.expectation(description: "not expected") + expectation.isInverted = true + + // Given + let swizzler = URLSessionTaskDelegateSwizzler() + + try swizzler.swizzle( + delegateClass: MockDelegate.self, + interceptDidFinishCollecting: { _, _, _ in + expectation.fulfill() + }, + interceptDidCompleteWithError: { _, _, _ in + expectation.fulfill() + } + ) + + // When + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // not intercepted + + swizzler.unswizzle() + + waitForExpectations(timeout: 5) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskInterceptionTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskInterceptionTests.swift new file mode 100644 index 0000000000..fd30c513f4 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskInterceptionTests.swift @@ -0,0 +1,219 @@ +/* + * 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 URLSessionTaskInterceptionTests: XCTestCase { + func testWhenInterceptionIsCreated_itHasUniqueIdentifier() { + // When + let interception1 = URLSessionTaskInterception(request: .mockAny(), isFirstParty: true) + let interception2 = URLSessionTaskInterception(request: .mockAny(), isFirstParty: false) + + // Then + XCTAssertNotEqual(interception1.identifier, interception2.identifier) + } + + func testWhenInterceptionReceivesData_itAppendsItToPreviousData() { + let chunk1 = "abc".utf8Data + let chunk2 = "def".utf8Data + let chunk3 = "ghi".utf8Data + + let interception = URLSessionTaskInterception(request: .mockAny(), isFirstParty: .random()) + XCTAssertNil(interception.data) + + // When + interception.register(nextData: chunk1) + let data1 = interception.data + + interception.register(nextData: chunk2) + let data2 = interception.data + + interception.register(nextData: chunk3) + let data3 = interception.data + + // Then + XCTAssertEqual(data1, chunk1) + XCTAssertEqual(data2, chunk1 + chunk2) + XCTAssertEqual(data3, chunk1 + chunk2 + chunk3) + } + + func testWhenInterceptionReceivesBothMetricsAndCompletion_itIsConsideredDone() { + let interception = URLSessionTaskInterception(request: .mockAny(), isFirstParty: .mockAny()) + + // When + interception.register(response: .mockAny(), error: nil) + XCTAssertFalse(interception.isDone) + interception.register(metrics: .mockAny()) + + // Then + XCTAssertTrue(interception.isDone) + } +} + +class ResourceMetricsTests: XCTestCase { + func testCalculatingMetricDuration() { + let date = Date() + let metric = ResourceMetrics.DateInterval(start: date, end: date.addingTimeInterval(2)) + XCTAssertEqual(metric.duration, 2) + } + + func testWhenTaskMakesSingleFetchFromNetwork_thenAllMetricsExceptRedirectionAreCollected() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + + let taskInterval = DateInterval( + start: .mockDecember15th2019At10AMUTC(), + end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 5) + ) + let taskTransaction: URLSessionTaskTransactionMetrics = .mockBySpreadingDetailsBetween( + start: taskInterval.start, + end: taskInterval.end, + resourceFetchType: .networkLoad + ) + + // When + let taskMetrics: URLSessionTaskMetrics = .mockWith( + taskInterval: taskInterval, + transactionMetrics: [taskTransaction] + ) + + // Then + let resourceMetrics = ResourceMetrics(taskMetrics: taskMetrics) + XCTAssertEqual(resourceMetrics.fetch.start, taskInterval.start) + XCTAssertEqual(resourceMetrics.fetch.end, taskInterval.end) + XCTAssertNil(resourceMetrics.redirection, "Single-transaction task should not have redirection phase.") + XCTAssertEqual(resourceMetrics.dns?.start, taskTransaction.domainLookupStartDate!) + XCTAssertEqual(resourceMetrics.dns?.end, taskTransaction.domainLookupEndDate!) + XCTAssertEqual(resourceMetrics.connect?.start, taskTransaction.connectStartDate!) + XCTAssertEqual(resourceMetrics.connect?.end, taskTransaction.connectEndDate!) + XCTAssertEqual(resourceMetrics.ssl?.start, taskTransaction.secureConnectionStartDate!) + XCTAssertEqual(resourceMetrics.ssl?.end, taskTransaction.secureConnectionEndDate!) + XCTAssertEqual(resourceMetrics.firstByte?.start, taskTransaction.requestStartDate!) + XCTAssertEqual(resourceMetrics.firstByte?.end, taskTransaction.responseStartDate!) + XCTAssertEqual(resourceMetrics.download?.start, taskTransaction.responseStartDate!) + XCTAssertEqual(resourceMetrics.download?.end, taskTransaction.responseEndDate!) + XCTAssertEqual(resourceMetrics.responseSize, taskTransaction.countOfResponseBodyBytesAfterDecoding) + } + + func testWhenTaskMakesMultipleFetchesFromNetwork_thenAllMetricsAreCollected() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + + let taskInterval = DateInterval( + start: .mockDecember15th2019At10AMUTC(), + end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 10) + ) + // Transaction 1 spreads from 0% to 30% of the overall task duration. + let transaction1: URLSessionTaskTransactionMetrics = .mockBySpreadingDetailsBetween( + start: taskInterval.start, + end: taskInterval.start.addingTimeInterval(taskInterval.duration * 0.30), + resourceFetchType: .networkLoad + ) + // Transaction 2 spreads from 35% to 60% of the overall task duration. + let transaction2: URLSessionTaskTransactionMetrics = .mockBySpreadingDetailsBetween( + start: taskInterval.start.addingTimeInterval(taskInterval.duration * 0.35), + end: taskInterval.start.addingTimeInterval(taskInterval.duration * 0.60), + resourceFetchType: .networkLoad + ) + // Transaction 3 spreads from 65% to 100% of the overall task duration. + let transaction3: URLSessionTaskTransactionMetrics = .mockBySpreadingDetailsBetween( + start: taskInterval.start.addingTimeInterval(taskInterval.duration * 0.65), + end: taskInterval.start.addingTimeInterval(taskInterval.duration), + resourceFetchType: .networkLoad + ) + + // When + let taskMetrics: URLSessionTaskMetrics = .mockWith( + taskInterval: taskInterval, + transactionMetrics: [transaction1, transaction2, transaction3] + ) + + // Then + let resourceMetrics = ResourceMetrics(taskMetrics: taskMetrics) + XCTAssertEqual(resourceMetrics.fetch.start, taskInterval.start) + XCTAssertEqual(resourceMetrics.fetch.end, taskInterval.end) + XCTAssertEqual( + resourceMetrics.redirection?.start, + transaction1.fetchStartDate!, + "Redirection should start with from 1st transaction" + ) + XCTAssertEqual( + resourceMetrics.redirection?.end, + transaction2.responseEndDate!, + "Redirection should end with from 2nd transaction" + ) + XCTAssertEqual(resourceMetrics.dns?.start, transaction3.domainLookupStartDate!) + XCTAssertEqual(resourceMetrics.dns?.end, transaction3.domainLookupEndDate!) + XCTAssertEqual(resourceMetrics.connect?.start, transaction3.connectStartDate!) + XCTAssertEqual(resourceMetrics.connect?.end, transaction3.connectEndDate!) + XCTAssertEqual(resourceMetrics.ssl?.start, transaction3.secureConnectionStartDate!) + XCTAssertEqual(resourceMetrics.ssl?.end, transaction3.secureConnectionEndDate!) + XCTAssertEqual(resourceMetrics.firstByte?.start, transaction3.requestStartDate!) + XCTAssertEqual(resourceMetrics.firstByte?.end, transaction3.responseStartDate!) + XCTAssertEqual(resourceMetrics.download?.start, transaction3.responseStartDate!) + XCTAssertEqual(resourceMetrics.download?.end, transaction3.responseEndDate!) + XCTAssertEqual(resourceMetrics.responseSize, transaction3.countOfResponseBodyBytesAfterDecoding) + } + + func testWhenTaskMakesFetchFromLocalCache_thenOnlyFetchMetricIsCollected() { + guard #available(iOS 13, tvOS 13, *) else { + return + } + + let taskInterval = DateInterval( + start: .mockDecember15th2019At10AMUTC(), + end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 5) + ) + let taskTransaction: URLSessionTaskTransactionMetrics = .mockBySpreadingDetailsBetween( + start: taskInterval.start, + end: taskInterval.end, + resourceFetchType: .localCache + ) + + // When + let taskMetrics: URLSessionTaskMetrics = .mockWith( + taskInterval: taskInterval, + transactionMetrics: [taskTransaction] + ) + + // Then + let resourceMetrics = ResourceMetrics(taskMetrics: taskMetrics) + XCTAssertEqual(resourceMetrics.fetch.start, taskInterval.start) + XCTAssertEqual(resourceMetrics.fetch.end, taskInterval.end) + XCTAssertNil( + resourceMetrics.redirection, + "`redirection` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.dns, + "`dns` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.connect, + "`connect` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.ssl, + "`ssl` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.firstByte, + "`firstByte` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.download, + "`download` should not be tracked for cache transactions." + ) + XCTAssertNil( + resourceMetrics.responseSize, + "`responseSize` should not be tracked for cache transactions." + ) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift new file mode 100644 index 0000000000..dc8f20b952 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/URLSessionTaskSwizzlerTests.swift @@ -0,0 +1,40 @@ +/* + * 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 URLSessionTaskSwizzlerTests: XCTestCase { + func testSwizzling_taskResume() throws { + let expectation = self.expectation(description: "resume") + + // Given + let swizzler = URLSessionTaskSwizzler() + + try swizzler.swizzle( + interceptResume: { _ in + expectation.fulfill() + } + ) + + // When + let session = URLSession(configuration: .ephemeral) + let url = URL(string: "https://www.datadoghq.com/")! + session + .dataTask(with: url) + .resume() // intercepted + + swizzler.unswizzle() + + session + .dataTask(with: url) + .resume() // not intercepted + + // Then + wait(for: [expectation], timeout: 5) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift new file mode 100644 index 0000000000..ffb69e103d --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift @@ -0,0 +1,55 @@ +/* + * 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 + +class W3CHTTPHeadersReaderTests: XCTestCase { + func testW3CHTTPHeadersReaderReadsSingleHeader() { + let w3cHTTPHeadersReader = W3CHTTPHeadersReader(httpHeaderFields: ["traceparent": "00-4d2-929-01"]) + let ids = w3cHTTPHeadersReader.read() + + XCTAssertEqual(ids?.traceID, TraceID(idLo: 1_234)) + XCTAssertEqual(ids?.spanID, SpanID(2_345)) + XCTAssertNil(ids?.parentSpanID) + } + + func testW3CHTTPHeadersReaderReadsSingleHeaderWithSampling() { + let w3cHTTPHeadersReader = W3CHTTPHeadersReader(httpHeaderFields: ["traceparent": "00-0-0-00"]) + let ids = w3cHTTPHeadersReader.read() + + XCTAssertNil(ids?.traceID) + XCTAssertNil(ids?.spanID) + XCTAssertNil(ids?.parentSpanID) + } + + func testReadingSampledTraceContext() { + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsAll() { + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .all) + writer.write(traceContext: .mockRandom()) + + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) + } + + func testReadingNotSampledTraceContext_givenTraceContextInjectionIsSampled() { + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), traceContextInjection: .sampled) + writer.write(traceContext: .mockRandom()) + + let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) + XCTAssertNil(reader.read(), "When not sampled, it should not return trace context") + XCTAssertNil(reader.sampled) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift new file mode 100644 index 0000000000..598de0fb94 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift @@ -0,0 +1,101 @@ +/* + * 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 + +class W3CHTTPHeadersWriterTests: XCTestCase { + func testWritingSampledTraceContext_withHeadBasedSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .headBased, + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ], + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") + } + + func testWritingDroppedTraceContext_withHeadBasedSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .headBased, + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ], + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ], + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") + } + + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ], + traceContextInjection: .all + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") + } +} diff --git a/DatadogInternal/Tests/Swizzling/MethodSwizzlerTests.swift b/DatadogInternal/Tests/Swizzling/MethodSwizzlerTests.swift new file mode 100644 index 0000000000..42756df053 --- /dev/null +++ b/DatadogInternal/Tests/Swizzling/MethodSwizzlerTests.swift @@ -0,0 +1,194 @@ +/* + * 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 + +@objc +private class BaseClass: NSObject { + @objc + func methodToSwizzle() -> String { + "original" + } +} + +private class Swizzler: MethodSwizzler<@convention(c) (AnyObject, Selector) -> String, @convention(block) (AnyObject) -> String> { + static let selector = #selector(BaseClass.methodToSwizzle) + + let method: Method + + init(method: Method) { + self.method = method + } + + init(_ cls: BaseClass.Type = BaseClass.self, _ name: Selector = Swizzler.selector) throws { + method = try dd_class_getInstanceMethod(cls, name) + } + + func swizzle(callback: @escaping () -> Void) { + self.swizzle(method) { currentImp in + return { impSelf in + callback() + return currentImp(impSelf, Swizzler.selector) + } + } + } + + func swizzle(override: @escaping (String) -> String) { + self.swizzle(method) { currentImp in + return { impSelf in + return override(currentImp(impSelf, Swizzler.selector)) + } + } + } +} + +class MethodSwizzlerTests: XCTestCase { + func test_simpleSwizzle() throws { + let swizzler = try Swizzler() + let obj = BaseClass() + + // before + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original") + + // swizzle + swizzler.swizzle { $0 + .mockAny() } + + // after + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original" + .mockAny()) + swizzler.unswizzle() + } + + func test_searchWrongSelector() { + let wrongSelToSwizzle = Selector(("selector_who_never_existed")) + + let expectedErrorDescription = "\(NSStringFromSelector(wrongSelToSwizzle)) is not found in \(NSStringFromClass(BaseClass.self))" + XCTAssertThrowsError(try dd_class_getInstanceMethod(BaseClass.self, wrongSelToSwizzle), "Wrong selector should throw") { error in + let internalError = error as? InternalError + XCTAssertEqual(internalError?.description, expectedErrorDescription) + } + } + + func test_findSubclassMethod() throws { + class EmptySubclass: BaseClass { } + class EmptySubSubclass: EmptySubclass { } + XCTAssertNotNil(try dd_class_getInstanceMethod(EmptySubclass.self, Swizzler.selector)) + XCTAssertNotNil(try dd_class_getInstanceMethod(EmptySubSubclass.self, Swizzler.selector)) + } + + func test_multiple_swizzle() throws { + let method = try dd_class_getInstanceMethod(BaseClass.self, Swizzler.selector) + let swizzler1 = Swizzler(method: method) + let swizzler2 = Swizzler(method: method) + + let obj = BaseClass() + let before_imp = method_getImplementation(method) + + // first swizzling + swizzler1.swizzle { $0 + ", first" } + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original, first") + + // second swizzling + swizzler2.swizzle { $0 + ", second" } + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original, first, second") + + // third swizzling + swizzler1.swizzle { $0 + ", third" } + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original, first, second, third") + + // remove second swizzling + swizzler2.unswizzle() + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original, first, third") + + // revert to original imp + swizzler1.unswizzle() + let after_imp = method_getImplementation(method) + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original") + XCTAssertEqual(before_imp, after_imp) + } + + func test_swizzle_count() throws { + class Subclass: BaseClass { + override func methodToSwizzle() -> String { "subclass" } + } + class SubSubclass: Subclass { + override func methodToSwizzle() -> String { "subsubclass" } + } + + // Given + let method1 = try dd_class_getInstanceMethod(BaseClass.self, Swizzler.selector) + let method2 = try dd_class_getInstanceMethod(Subclass.self, Swizzler.selector) + let method3 = try dd_class_getInstanceMethod(SubSubclass.self, Swizzler.selector) + + let swizzler1 = Swizzler(method: method1) + let swizzler2 = Swizzler(method: method2) + let swizzler3 = Swizzler(method: method3) + + // When + swizzler1.swizzle { } + XCTAssertEqual(Swizzling.methods.count, 1) + swizzler2.swizzle { } + XCTAssertEqual(Swizzling.methods.count, 2) + swizzler3.swizzle { } + XCTAssertEqual(Swizzling.methods.count, 3) + + // Then + XCTAssertEqual(Swizzling.description, "[methodToSwizzle, methodToSwizzle, methodToSwizzle]") + + // When + swizzler1.unswizzle() + XCTAssertEqual(Swizzling.methods.count, 2) + swizzler2.unswizzle() + XCTAssertEqual(Swizzling.methods.count, 1) + swizzler3.unswizzle() + XCTAssertEqual(Swizzling.methods.count, 0) + + // Then + XCTAssertEqual(Swizzling.description, "[]") + } + + func test_swizzle_concurrently() throws { + // swiftlint:disable opening_brace + + // Given + let method = try dd_class_getInstanceMethod(BaseClass.self, Swizzler.selector) + let swizzler1 = Swizzler(method: method) + let swizzler2 = Swizzler(method: method) + let swizzler3 = Swizzler(method: method) + + let before_imp = method_getImplementation(method) + var callstack: [String] = [] + + // When + callConcurrently( + { swizzler1.swizzle { callstack.append("1.1") } }, + { swizzler1.swizzle { callstack.append("1.2") } }, + { swizzler2.swizzle { callstack.append("2") } }, + { swizzler3.swizzle { callstack.append("3") } } + ) + + // Then + let obj = BaseClass() + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original") + + callstack.sort() + XCTAssertEqual(callstack, ["1.1", "1.2", "2", "3"]) + + // When + callstack = [] + callConcurrently( + { swizzler1.unswizzle() }, + { swizzler2.unswizzle() }, + { swizzler3.unswizzle() } + ) + XCTAssertEqual(obj.perform(Swizzler.selector)?.takeUnretainedValue() as? String, "original") + XCTAssertEqual(callstack, []) + + let after_imp = method_getImplementation(method) + XCTAssertEqual(before_imp, after_imp) + // swiftlint:enable opening_brace + } +} diff --git a/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift new file mode 100644 index 0000000000..23a1271f59 --- /dev/null +++ b/DatadogInternal/Tests/Telemetry/TelemetryMocks.swift @@ -0,0 +1,67 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import TestUtilities + +@testable import DatadogInternal + +extension ConfigurationTelemetry { + static func mockRandom() -> Self { + ConfigurationTelemetry( + actionNameAttribute: .mockRandom(), + allowFallbackToLocalStorage: .mockRandom(), + allowUntrustedEvents: .mockRandom(), + appHangThreshold: .mockRandom(), + backgroundTasksEnabled: .mockRandom(), + batchProcessingLevel: .mockRandom(), + batchSize: .mockRandom(), + batchUploadFrequency: .mockRandom(), + dartVersion: .mockRandom(), + forwardErrorsToLogs: .mockRandom(), + defaultPrivacyLevel: .mockRandom(), + textAndInputPrivacyLevel: .mockRandom(), + imagePrivacyLevel: .mockRandom(), + touchPrivacyLevel: .mockRandom(), + initializationType: .mockRandom(), + mobileVitalsUpdatePeriod: .mockRandom(), + reactNativeVersion: .mockRandom(), + reactVersion: .mockRandom(), + sessionReplaySampleRate: .mockRandom(), + sessionSampleRate: .mockRandom(), + silentMultipleInit: .mockRandom(), + startRecordingImmediately: .mockRandom(), + telemetryConfigurationSampleRate: .mockRandom(), + telemetrySampleRate: .mockRandom(), + tracerAPI: .mockRandom(), + tracerAPIVersion: .mockRandom(), + traceSampleRate: .mockRandom(), + trackBackgroundEvents: .mockRandom(), + trackCrossPlatformLongTasks: .mockRandom(), + trackErrors: .mockRandom(), + trackFlutterPerformance: .mockRandom(), + trackFrustrations: .mockRandom(), + trackLongTask: .mockRandom(), + trackNativeErrors: .mockRandom(), + trackNativeLongTasks: .mockRandom(), + trackNativeViews: .mockRandom(), + trackNetworkRequests: .mockRandom(), + trackResources: .mockRandom(), + trackSessionAcrossSubdomains: .mockRandom(), + trackUserInteractions: .mockRandom(), + trackViewsManually: .mockRandom(), + unityVersion: .mockRandom(), + useAllowedTracingUrls: .mockRandom(), + useBeforeSend: .mockRandom(), + useExcludedActivityUrls: .mockRandom(), + useFirstPartyHosts: .mockRandom(), + useLocalEncryption: .mockRandom(), + useProxy: .mockRandom(), + useSecureSessionCookie: .mockRandom(), + useTracing: .mockRandom(), + useWorkerUrl: .mockRandom() + ) + } +} diff --git a/DatadogInternal/Tests/Telemetry/TelemetryTests.swift b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift new file mode 100644 index 0000000000..4b3a45a20a --- /dev/null +++ b/DatadogInternal/Tests/Telemetry/TelemetryTests.swift @@ -0,0 +1,209 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import Foundation +import TestUtilities +@testable import DatadogInternal + +class TelemetryTests: XCTestCase { + private let telemetry = TelemetryMock() + + // MARK: - Debug Telemetry + + func testSendingDebugTelemetry() throws { + // When + #sourceLocation(file: "File.swift", line: 1) + telemetry.debug("debug message", attributes: ["foo": "bar"]) + #sourceLocation() + + // Then + let debug = try XCTUnwrap(telemetry.messages.firstDebug()) + XCTAssertEqual(debug.id, "\(moduleName())/File.swift:1:debug message") + XCTAssertEqual(debug.message, "debug message") + XCTAssertEqual(debug.attributes as? [String: String], ["foo": "bar"]) + XCTAssertEqual(telemetry.messages.count, 1) + } + + // MARK: - Error Telemetry + + func testSendingErrorTelemetry() throws { + // When + #sourceLocation(file: "File.swift", line: 1) + telemetry.error("error message", kind: "error.kind", stack: "error.stack") + #sourceLocation() + + // Then + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertEqual(error.id, "\(moduleName())/File.swift:1:error message") + XCTAssertEqual(error.message, "error message") + XCTAssertEqual(error.kind, "error.kind") + XCTAssertEqual(error.stack, "error.stack") + XCTAssertEqual(telemetry.messages.count, 1) + } + + func testSendingErrorTelemetry_whenNoKindAndNoStack() throws { + // When + #sourceLocation(file: "File.swift", line: 1) + telemetry.error("error message") + #sourceLocation() + + // Then + let error = try XCTUnwrap(telemetry.messages.firstError()) + XCTAssertEqual(error.id, "\(moduleName())/File.swift:1:error message") + XCTAssertEqual(error.message, "error message") + XCTAssertEqual(error.kind, "\(moduleName())/File.swift") + XCTAssertEqual(error.stack, "\(moduleName())/File.swift:1") + XCTAssertEqual(telemetry.messages.count, 1) + } + + func testSendingErrorTelemetry_withSwiftError() throws { + // Given + struct SwiftError: Error { + let description = "error description" + } + let swiftError = SwiftError() + + // When + telemetry.error(swiftError) + telemetry.error("custom message", error: swiftError) + + // Then + let errors = telemetry.messages.compactMap({ $0.asError }) + XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(errors[0].message, #"SwiftError(description: "error description")"#) + XCTAssertEqual(errors[0].kind, "SwiftError") + XCTAssertEqual(errors[0].stack, #"SwiftError(description: "error description")"#) + XCTAssertEqual(errors[1].message, #"custom message - SwiftError(description: "error description")"#) + XCTAssertEqual(errors[1].kind, "SwiftError") + XCTAssertEqual(errors[1].stack, #"SwiftError(description: "error description")"#) + } + + func testSendingErrorTelemetry_withNSError() throws { + // Given + let nsError = NSError( + domain: "custom-domain", + code: 10, + userInfo: [NSLocalizedDescriptionKey: "error description"] + ) + + // When + telemetry.error(nsError) + telemetry.error("custom message", error: nsError) + + // Then + let errors = telemetry.messages.compactMap({ $0.asError }) + XCTAssertEqual(telemetry.messages.count, 2) + XCTAssertEqual(errors[0].message, "error description") + XCTAssertEqual(errors[0].kind, "custom-domain - 10") + XCTAssertEqual(errors[0].stack, #"Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description}"#) + XCTAssertEqual(errors[1].message, "custom message - error description") + XCTAssertEqual(errors[1].kind, "custom-domain - 10") + XCTAssertEqual(errors[1].stack, #"Error Domain=custom-domain Code=10 "error description" UserInfo={NSLocalizedDescription=error description}"#) + } + + // MARK: - Configuration Telemetry + + func testSendingConfigurationTelemetry() throws { + // When + 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"], 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 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 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 { + let operationName: String = .mockRandom() + let callerClass: String = .mockRandom() + let isSuccessful: Bool = .random() + + // When + 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 + + func testWhenUsingCoreTelemetry_itSendsTelemetryToMessageReceiver() throws { + let receiver = FeatureMessageReceiverMock() + let core = PassthroughCoreMock(messageReceiver: receiver) + + core.telemetry.debug("debug message") + XCTAssertEqual(receiver.messages.lastTelemetry?.asDebug?.message, "debug message") + + core.telemetry.error("error message") + XCTAssertEqual(receiver.messages.lastTelemetry?.asError?.message, "error message") + + core.telemetry.configuration(batchSize: 123) + XCTAssertEqual(receiver.messages.lastTelemetry?.asConfiguration?.batchSize, 123) + + 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(), headSampleRate: 100) + core.telemetry.stopMethodCalled(metricTrace) + XCTAssertEqual(receiver.messages.lastTelemetry?.asMetric?.name, MethodCalledMetric.name) + } +} diff --git a/DatadogInternal/Tests/Upload/DataCompressionTests.swift b/DatadogInternal/Tests/Upload/DataCompressionTests.swift new file mode 100644 index 0000000000..647ccc6bbb --- /dev/null +++ b/DatadogInternal/Tests/Upload/DataCompressionTests.swift @@ -0,0 +1,68 @@ +/* + * 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 DataCompressionTests: XCTestCase { + let encoder = JSONEncoder() + + struct Foo: Codable { + let bar: String + let baz: Int + let qux: URL + + init() { + let length: Int = .mockRandom(min: 100, max: 10_000) + bar = .mockRandom(length: length) + baz = .mockRandom() + qux = .mockRandom() + } + } + + func testWhenComputingAdler32Checksum_itAlwaysHas4Bytes() throws { + for _ in 1...100 { + // Given + let data = try encoder.encode(Foo()) + + // When + let checksum = Deflate.adler32(data) + + // Then + XCTAssertEqual(checksum?.count, 4) + } + } + + func testWhenDataIsDeflated_itInflateToOriginalData() throws { + for _ in 1...100 { + // Given + let data = try encoder.encode(Foo()) + + // When + let compressed = try XCTUnwrap(Deflate.compress(data)) + let decompressed = zlib.decompress(compressed) + + // Then + XCTAssertEqual(decompressed, data) + } + } + + func testWhenDataIsCompressed_itDecompressToOriginalData() throws { + for _ in 1...100 { + // Given + let data = try encoder.encode(Foo()) + + // When + let compressed = try XCTUnwrap(Deflate.encode(data)) + let decompressed = zlib.decode(compressed) + + // Then + XCTAssertEqual(decompressed, data) + } + } +} diff --git a/DatadogInternal/Tests/Upload/DataFormatTests.swift b/DatadogInternal/Tests/Upload/DataFormatTests.swift new file mode 100644 index 0000000000..5488942b8c --- /dev/null +++ b/DatadogInternal/Tests/Upload/DataFormatTests.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 +@testable import DatadogInternal + +final class DataFormatTests: XCTestCase { + func testFormat() throws { + let format = DataFormat(prefix: "prefix", suffix: "suffix", separator: "\n") + let events = [ + "abc".data(using: .utf8)!, + "def".data(using: .utf8)!, + "ghi".data(using: .utf8)! + ] + let formatted = format.format(events) + let actual = String(data: formatted, encoding: .utf8)! + let expected = + """ + prefixabc + def + ghisuffix + """ + XCTAssertEqual(actual, expected) + } +} 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/DatadogInternal/Tests/Utils/SamplerTests.swift b/DatadogInternal/Tests/Utils/SamplerTests.swift new file mode 100644 index 0000000000..d60ce3a4ce --- /dev/null +++ b/DatadogInternal/Tests/Utils/SamplerTests.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 XCTest +@testable import DatadogInternal + + class SamplerTests: XCTestCase { + private let measurements = 0..<500 + + func testWhenSamplingRateIs0_itAlwaysReturnsNotSampled() { + // Given + let sampler = Sampler(samplingRate: 0) + + // When + var notSampledCount = 0 + measurements.forEach { _ in + notSampledCount += sampler.sample() ? 0 : 1 + } + + // Then + XCTAssertEqual(notSampledCount, measurements.count) + } + + func testWhenSamplingRateIs100_itAlwaysReturnsIsSampled() { + // Given + let sampler = Sampler(samplingRate: 100) + + // When + var isSampledCount = 0 + measurements.forEach { _ in + isSampledCount += sampler.sample() ? 1 : 0 + } + + // Then + XCTAssertEqual(isSampledCount, measurements.count) + } + + func testWhenSamplingRateIsLow_itReturnsNotSampledMoreOften() { + // Given + let sampler = Sampler(samplingRate: .random(in: (0..<30))) + + // When + var isSampledCount = 0 + var notSampledCount = 0 + measurements.forEach { _ in + let value = sampler.sample() + isSampledCount += value ? 1 : 0 + notSampledCount += value ? 0 : 1 + } + + // Then + XCTAssertGreaterThan(notSampledCount, isSampledCount) + } + + func testWhenSamplingRateIsHigh_itReturnsNotSampledMoreOften() { + // Given + let sampler = Sampler(samplingRate: .random(in: (70..<100))) + + // When + var isSampledCount = 0 + var notSampledCount = 0 + measurements.forEach { _ in + let value = sampler.sample() + isSampledCount += value ? 1 : 0 + notSampledCount += value ? 0 : 1 + } + + // Then + XCTAssertGreaterThan(isSampledCount, notSampledCount) + } + + func testWhenInitializing_itSanitizesSamplingRateToAllowedRange() { + measurements.forEach { _ in + // Given + let randomRate: Float = .random(in: -100..<200) + + // When + let sampler = Sampler(samplingRate: randomRate) + + // Then + XCTAssertGreaterThanOrEqual(sampler.samplingRate, 0) + XCTAssertLessThanOrEqual(sampler.samplingRate, 100) + } + } + } diff --git a/DatadogInternal/Tests/Utils/SwiftExtensionsTests.swift b/DatadogInternal/Tests/Utils/SwiftExtensionsTests.swift new file mode 100644 index 0000000000..401df7277d --- /dev/null +++ b/DatadogInternal/Tests/Utils/SwiftExtensionsTests.swift @@ -0,0 +1,128 @@ +/* + * 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 TimeIntervalExtensionTests: XCTestCase { + func testTimeIntervalFromMilliseconds() { + let milliseconds: Int64 = 1_576_404_000_000 + + let timeInterval = TimeInterval(fromMilliseconds: milliseconds) + let date = Date(timeIntervalSince1970: timeInterval) + XCTAssertEqual(date, Date.mockDecember15th2019At10AMUTC()) + } + + func testTimeIntervalSince1970InMilliseconds() { + let date15Dec2019 = Date.mockDecember15th2019At10AMUTC() + XCTAssertEqual(date15Dec2019.timeIntervalSince1970.toMilliseconds, 1_576_404_000_000) + + let dateAdvanced = date15Dec2019 + 9.999 + XCTAssertEqual(dateAdvanced.timeIntervalSince1970.toMilliseconds, 1_576_404_009_999) + + let dateAgo = date15Dec2019 - 0.001 + XCTAssertEqual(dateAgo.timeIntervalSince1970.toMilliseconds, 1_576_403_999_999) + + let overflownDate = Date(timeIntervalSinceReferenceDate: .greatestFiniteMagnitude) + XCTAssertEqual(overflownDate.timeIntervalSince1970.toMilliseconds, UInt64.max) + + let uInt64MaxDate = Date(timeIntervalSinceReferenceDate: TimeInterval(UInt64.max)) + XCTAssertEqual(uInt64MaxDate.timeIntervalSince1970.toMilliseconds, UInt64.max) + } + + func testTimeIntervalSince1970InNanoseconds() { + let date15Dec2019 = Date.mockDecember15th2019At10AMUTC() + XCTAssertEqual(date15Dec2019.timeIntervalSince1970.toNanoseconds, 1_576_404_000_000_000_000) + + // As `TimeInterval` yields sub-millisecond precision this rounds up to the nearest millisecond: + let dateAdvanced = date15Dec2019 + 9.999999999 + XCTAssertEqual(dateAdvanced.timeIntervalSince1970.toNanoseconds, 1_576_404_010_000_000_000) + + // As `TimeInterval` yields sub-millisecond precision this rounds up to the nearest millisecond: + let dateAgo = date15Dec2019 - 0.000000001 + XCTAssertEqual(dateAgo.timeIntervalSince1970.toNanoseconds, 1_576_404_000_000_000_000) + + let overflownDate = Date(timeIntervalSinceReferenceDate: .greatestFiniteMagnitude) + XCTAssertEqual(overflownDate.timeIntervalSince1970.toNanoseconds, UInt64.max) + + let uInt64MaxDate = Date(timeIntervalSinceReferenceDate: TimeInterval(UInt64.max)) + XCTAssertEqual(uInt64MaxDate.timeIntervalSince1970.toNanoseconds, UInt64.max) + } +} + +class UUIDExtensionTests: XCTestCase { + func testNullUUID() { + let uuid: UUID = .nullUUID + XCTAssertEqual(uuid.uuidString, "00000000-0000-0000-0000-000000000000", "It must be all zeroes") + } +} + +class IntegerOverflowExtensionTests: XCTestCase { + func testHappyPath() { + let reasonableDouble = Double(1_000.123456) + + XCTAssertNoThrow(try UInt64(withReportingOverflow: reasonableDouble)) + XCTAssertEqual(try UInt64(withReportingOverflow: reasonableDouble), 1_000) + } + + func testNegative() { + let negativeDouble = Double(-1_000.123456) + + XCTAssertThrowsError(try UInt64(withReportingOverflow: negativeDouble)) { error in + XCTAssertTrue(error is FixedWidthIntegerError) + if case let FixedWidthIntegerError.overflow(overflowingValue) = (error as! FixedWidthIntegerError) { + XCTAssertEqual(overflowingValue, negativeDouble) + } + } + } + + func testFloat() { + let simpleFloat = Float(222.123456) + + XCTAssertNoThrow(try UInt8(withReportingOverflow: simpleFloat)) + XCTAssertEqual(try UInt8(withReportingOverflow: simpleFloat), 222) + } + + func testGreatestFiniteMagnitude() { + let almostInfinity = Double.greatestFiniteMagnitude + + XCTAssertThrowsError(try UInt64(withReportingOverflow: almostInfinity)) { error in + XCTAssertTrue(error is FixedWidthIntegerError) + } + } + + func testInfinity() { + let infinityAndBeyond = Double.infinity + + XCTAssertThrowsError(try UInt64(withReportingOverflow: infinityAndBeyond)) { error in + XCTAssertTrue(error is FixedWidthIntegerError) + } + } + + func testCornerCase() { + let uInt64Max = Double(UInt64.max) + + XCTAssertThrowsError(try UInt64(withReportingOverflow: uInt64Max)) { error in + XCTAssertTrue(error is FixedWidthIntegerError) + if case let FixedWidthIntegerError.overflow(overflowingValue) = (error as! FixedWidthIntegerError) { + XCTAssertEqual(overflowingValue, uInt64Max) + } + } + } +} + +class DoubleExtensionTests: XCTestCase { + func testDivideIfNotZero() { + XCTAssertNil(2.0.divideIfNotZero(by: 0)) + XCTAssertEqual(2.0.divideIfNotZero(by: 1.0), 2.0) + } + + func testInverted() { + XCTAssertEqual(0.0.inverted, 0.0) + XCTAssertEqual(2.0.inverted, 0.5) + } +} diff --git a/DatadogLogs.podspec b/DatadogLogs.podspec new file mode 100644 index 0000000000..7eed5cf060 --- /dev/null +++ b/DatadogLogs.podspec @@ -0,0 +1,28 @@ +Pod::Spec.new do |s| + s.name = "DatadogLogs" + s.version = "2.22.0" + s.summary = "Datadog Logs Module." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + 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 } + + s.source_files = ["DatadogLogs/Sources/**/*.swift"] + + s.dependency 'DatadogInternal', s.version.to_s + +end diff --git a/DatadogLogs/Sources/ConsoleLogger.swift b/DatadogLogs/Sources/ConsoleLogger.swift new file mode 100644 index 0000000000..4e42d1b0ae --- /dev/null +++ b/DatadogLogs/Sources/ConsoleLogger.swift @@ -0,0 +1,103 @@ +/* + * 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 + +/// `Logger` printing logs to console. +internal final class ConsoleLogger: LoggerProtocol { + struct Configuration { + /// Time zone for rendering logs. + let timeZone: TimeZone + /// The format of rendering logs in console. + let format: Logger.Configuration.ConsoleLogFormat + } + + /// Date provider for logs. + private let dateProvider: DateProvider + /// Time formatter for rendering log's date in console. + private let timeFormatter: DateFormatterType + /// The prefix to use when rendering log. + private let prefix: String + /// The function used to render log. + private let printFunction: @Sendable (String, CoreLoggerLevel) -> Void + + init( + configuration: Configuration, + dateProvider: DateProvider, + printFunction: @escaping @Sendable (String, CoreLoggerLevel) -> Void + ) { + self.dateProvider = dateProvider + self.timeFormatter = presentationDateFormatter(withTimeZone: configuration.timeZone) + + switch configuration.format { + case .short: + self.prefix = "" + case .shortWith(let prefix): + self.prefix = "\(prefix) " + } + + self.printFunction = printFunction + } + + // MARK: - Logging + + func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) { + var errorString: String? = nil + if let error = error { + let ddError = DDError(error: error) + errorString = buildErrorString(error: ddError) + } + + internalLog(level: level, message: message, errorString: errorString) + } + + private func internalLog(level: LogLevel, message: String, errorString: String?) { + let time = timeFormatter.string(from: dateProvider.now) + let status = level.asLogStatus.rawValue.uppercased() + + var log = "\(self.prefix)\(time) [\(status)] \(message)" + + if let errorString = errorString { + log += "\n\nError details:\n\(errorString)" + } + printFunction(log, CoreLoggerLevel(logLevel: level)) + } + + private func buildErrorString(error: DDError) -> String { + return """ + → type: \(error.type) + → message: \(error.message) + → stack: \(error.stack) + """ + } + + // MARK: - Attributes (no-op for this logger) + + func addAttribute(forKey key: AttributeKey, value: AttributeValue) {} + func removeAttribute(forKey key: AttributeKey) {} + + // MARK: - Tags (no-op for this logger) + + func addTag(withKey key: String, value: String) {} + func removeTag(withKey key: String) {} + func add(tag: String) {} + func remove(tag: String) {} +} + +extension ConsoleLogger: InternalLoggerProtocol { + func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String: Encodable]?) { + var errorString: String? = nil + if errorKind != nil || errorMessage != nil || stackTrace != nil { + // Cross platform frameworks don't necessarilly send all values for errors. Send empty strings + // for any values that are empty. + let ddError = DDError(type: errorKind ?? "", message: errorMessage ?? "", stack: stackTrace ?? "") + errorString = buildErrorString(error: ddError) + } + + internalLog(level: level, message: message, errorString: errorString) + } +} diff --git a/DatadogLogs/Sources/Feature/Baggages.swift b/DatadogLogs/Sources/Feature/Baggages.swift new file mode 100644 index 0000000000..0f820e1d5f --- /dev/null +++ b/DatadogLogs/Sources/Feature/Baggages.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 Foundation +import DatadogInternal + +/// Error message sent from Logs on the message-bus. +internal struct ErrorMessage: Encodable { + static let key = "error" + /// The time of the log + let time: Date + /// The Log error message + let message: String + /// The Log error type + let type: String? + /// The Log error stack + let stack: String? + /// The Log error stack + let source: String = "logger" + /// The Log attributes + let attributes: AnyEncodable + /// Binary images if need to decode the stack trace + let binaryImages: [BinaryImage]? +} + +internal struct GlobalLogAttributes: Codable { + static let key = "global-log-attributes" + + let attributes: [AttributeKey: AttributeValue] + + init(attributes: [AttributeKey: AttributeValue]) { + self.attributes = attributes + } + + func encode(to encoder: Encoder) throws { + var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) + try attributes.forEach { + let key = DynamicCodingKey($0) + try dynamicContainer.encode(AnyEncodable($1), forKey: key) + } + } + + init(from decoder: Decoder) throws { + // Decode other properties into [String: Codable] dictionary: + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + self.attributes = try dynamicContainer.allKeys + .reduce(into: [:]) { + $0[$1.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: $1) + } + } +} + +/// The Span context received from `DatadogCore`. +internal struct SpanContext: Decodable { + static let key = "span_context" + + enum CodingKeys: String, CodingKey { + case traceID = "dd.trace_id" + case spanID = "dd.span_id" + } + + let traceID: TraceID? + let spanID: SpanID? +} + +/// The RUM context received from `DatadogCore`. +internal struct RUMContext: Decodable { + static let key = "rum" + + enum CodingKeys: String, CodingKey { + case applicationID = "application.id" + case sessionID = "session.id" + case viewID = "view.id" + case userActionID = "user_action.id" + } + + /// Current RUM application ID - standard UUID string, lowecased. + let applicationID: String + /// Current RUM session ID - standard UUID string, lowecased. + let sessionID: String + /// Current RUM view ID - standard UUID string, lowecased. It can be empty when view is being loaded. + let viewID: String? + /// The ID of current RUM action (standard UUID `String`, lowercased). + let userActionID: String? +} diff --git a/DatadogLogs/Sources/Feature/LogsFeature.swift b/DatadogLogs/Sources/Feature/LogsFeature.swift new file mode 100644 index 0000000000..fa8159d2a4 --- /dev/null +++ b/DatadogLogs/Sources/Feature/LogsFeature.swift @@ -0,0 +1,64 @@ +/* + * 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 + +internal struct LogsFeature: DatadogRemoteFeature { + static let name = "logging" + + let requestBuilder: FeatureRequestBuilder + + let messageReceiver: FeatureMessageReceiver + + let logEventMapper: LogEventMapper? + + let backtraceReporter: BacktraceReporting? + + /// Global attributes attached to every log event. + let attributes: SynchronizedAttributes + + /// Time provider. + let dateProvider: DateProvider + + init( + logEventMapper: LogEventMapper?, + dateProvider: DateProvider, + customIntakeURL: URL? = nil, + telemetry: Telemetry = NOPTelemetry(), + backtraceReporter: BacktraceReporting? = nil + ) { + self.init( + logEventMapper: logEventMapper, + requestBuilder: RequestBuilder( + customIntakeURL: customIntakeURL, + telemetry: telemetry + ), + messageReceiver: CombinedFeatureMessageReceiver( + LogMessageReceiver(logEventMapper: logEventMapper), + CrashLogReceiver(dateProvider: dateProvider, logEventMapper: logEventMapper), + WebViewLogReceiver() + ), + dateProvider: dateProvider, + backtraceReporter: backtraceReporter + ) + } + + init( + logEventMapper: LogEventMapper?, + requestBuilder: FeatureRequestBuilder, + messageReceiver: FeatureMessageReceiver, + dateProvider: DateProvider, + backtraceReporter: BacktraceReporting? + ) { + self.logEventMapper = logEventMapper + self.requestBuilder = requestBuilder + self.messageReceiver = messageReceiver + self.dateProvider = dateProvider + self.backtraceReporter = backtraceReporter + self.attributes = SynchronizedAttributes(attributes: [:]) + } +} diff --git a/DatadogLogs/Sources/Feature/MessageReceivers.swift b/DatadogLogs/Sources/Feature/MessageReceivers.swift new file mode 100644 index 0000000000..78b0d6e556 --- /dev/null +++ b/DatadogLogs/Sources/Feature/MessageReceivers.swift @@ -0,0 +1,328 @@ +/* + * 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 + +/// Defines keys referencing RUM messages supported on the bus. +internal enum LoggingMessageKeys { + /// The key references a log entry message. + static let log = "log" + + /// The key references a crash message. + static let crash = "crash" +} + +/// Receiver to consume a Log message +internal struct LogMessageReceiver: FeatureMessageReceiver { + struct LogMessage: Decodable { + /// The Logger name + let logger: String + /// The Logger service + let service: String? + /// The Log date + let date: Date + /// The Log message + let message: String + /// The Log error + let error: DDError? + /// The Log level + let level: LogLevel + /// The thread name + let thread: String + /// The thread name + let networkInfoEnabled: Bool? + /// The Log user custom attributes + let userAttributes: [String: AnyCodable]? + /// The Log internal attributes + let internalAttributes: [String: AnyCodable]? + } + + /// The log event mapper + let logEventMapper: LogEventMapper? + + /// Process messages receives from the bus. + /// + /// - Parameters: + /// - message: The Feature message + /// - core: The core from which the message is transmitted. + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + do { + guard let log: LogMessage = try message.baggage(forKey: LoggingMessageKeys.log) else { + return false + } + + core.scope(for: LogsFeature.self).eventWriteContext { context, writer in + let builder = LogEventBuilder( + service: log.service ?? context.service, + loggerName: log.logger, + networkInfoEnabled: log.networkInfoEnabled ?? false, + eventMapper: logEventMapper + ) + + builder.createLogEvent( + date: log.date, + level: log.level, + message: log.message, + error: log.error, + errorFingerprint: nil, + binaryImages: nil, + attributes: .init( + userAttributes: log.userAttributes ?? [:], + internalAttributes: log.internalAttributes + ), + tags: [], + context: context, + threadName: log.thread, + callback: writer.write + ) + } + + return true + } catch { + core.telemetry + .error("Failed to decode log message in `LogMessageReceiver`", error: error) + } + + return false + } +} + +/// Receiver to consume a Crash Log message as Log. +internal struct CrashLogReceiver: FeatureMessageReceiver { + private struct Crash: Decodable { + /// The crash report. + let report: DDCrashReport + /// The crash context + let context: CrashContext + } + + private struct CrashContext: Decodable { + /// Interval between device and server time. + let serverTimeOffset: TimeInterval + /// The name of the service that data is generated from. + let service: String + /// The name of the environment that data is generated from. + let env: String + /// The version of the application that data is generated from. + let version: String + /// The build number of the application that data is generated from. + let buildNumber: String + /// Current device information. + let device: DeviceInfo + /// The version of Datadog iOS SDK. + let sdkVersion: String + /// Network information. + /// + /// Represents the current state of the device network connectivity and interface. + /// The value can be `unknown` if the network interface is not available or if it has not + /// yet been evaluated. + let networkConnectionInfo: NetworkConnectionInfo? + /// Carrier information. + /// + /// Represents the current telephony service info of the device. + /// This value can be `nil` of no service is currently registered, or if the device does + /// not support telephony services. + let carrierInfo: CarrierInfo? + /// Current user information. + let userInfo: UserInfo? + + /// A type representing part of last RUM view information required to link crash log with previous RUM session. + /// It mirrors the schema of `RUMViewEvent`, so we can decode it from the last `RUMViewEvent` coded in crash context. + struct PartialRUMViewEvent: Decodable { + struct Application: Decodable { + let id: String + } + struct Session: Decodable { + let id: String + } + struct View: Decodable { + let id: String + } + + let application: Application + let session: Session + let view: View + } + + struct GlobalLogAttributes: Decodable { + let attributes: [AttributeKey: AttributeValue] + + init(from decoder: Decoder) throws { + // Decode other properties into [String: Codable] dictionary: + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + self.attributes = try dynamicContainer.allKeys + .reduce(into: [:]) { + $0[$1.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: $1) + } + } + } + + /// The last RUM view in crashed app process. + let lastRUMViewEvent: PartialRUMViewEvent? + + /// Last global log attributes + let lastLogAttributes: GlobalLogAttributes? + } + + /// Time provider. + let dateProvider: DateProvider + let logEventMapper: LogEventMapper? + + /// Process messages receives from the bus. + /// + /// - Parameters: + /// - message: The Feature message + /// - core: The core from which the message is transmitted. + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + do { + guard let crash: Crash = try message.baggage(forKey: LoggingMessageKeys.crash) else { + return false + } + + return send(report: crash.report, with: crash.context, to: core) + } catch { + core.telemetry + .error("Failed to decode crash message in `LogMessageReceiver`", error: error) + } + return false + } + + private func send(report: DDCrashReport, with crashContext: CrashContext, to core: DatadogCoreProtocol) -> Bool { + // The `report.crashDate` uses system `Date` collected at the moment of crash, so we need to adjust it + // to the server time before processing. Following use of the current correction is not ideal, but this is the best + // approximation we can get. + let date = (report.date ?? dateProvider.now) + .addingTimeInterval(crashContext.serverTimeOffset) + + var errorAttributes: [AttributeKey: AttributeValue] = [:] + + // Set crash attributes for the error + errorAttributes[DDError.threads] = report.threads + errorAttributes[DDError.binaryImages] = report.binaryImages + errorAttributes[DDError.meta] = report.meta + errorAttributes[DDError.wasTruncated] = report.wasTruncated + + // Set RUM context if available (so emergency error is linked to the RUM session in Datadog app) + errorAttributes[LogEvent.Attributes.RUM.applicationID] = crashContext.lastRUMViewEvent?.application.id + errorAttributes[LogEvent.Attributes.RUM.sessionID] = crashContext.lastRUMViewEvent?.session.id + errorAttributes[LogEvent.Attributes.RUM.viewID] = crashContext.lastRUMViewEvent?.view.id + + let user = crashContext.userInfo + let deviceInfo = crashContext.device + + // 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. + core.scope(for: LogsFeature.self).eventWriteContext(bypassConsent: true) { context, writer in + let event = LogEvent( + date: date, + status: .emergency, + message: report.message, + error: .init( + kind: report.type, + message: report.message, + stack: report.stack, + sourceType: context.nativeSourceOverride ?? "ios" + ), + serviceName: crashContext.service, + environment: crashContext.env, + loggerName: "crash-reporter", + loggerVersion: crashContext.sdkVersion, + threadName: nil, + applicationVersion: crashContext.version, + applicationBuildNumber: crashContext.buildNumber, + buildId: nil, + variant: context.variant, + dd: .init( + device: .init( + brand: deviceInfo.brand, + name: deviceInfo.name, + model: deviceInfo.model, + architecture: deviceInfo.architecture + ) + ), + os: .init( + name: crashContext.device.osName, + version: crashContext.device.osVersion, + build: crashContext.device.osBuildNumber + ), + userInfo: .init( + id: user?.id, + name: user?.name, + email: user?.email, + extraInfo: user?.extraInfo ?? [:] + ), + networkConnectionInfo: crashContext.networkConnectionInfo, + mobileCarrierInfo: crashContext.carrierInfo, + attributes: .init( + userAttributes: userAttributes, + internalAttributes: errorAttributes + ), + tags: nil + ) + + logEventMapper?.map(event: event, callback: writer.write) ?? writer.write(value: event) + } + + return true + } +} + +/// Receiver to consume a Log event coming from Browser SDK. +internal struct WebViewLogReceiver: FeatureMessageReceiver { + /// Process messages receives from the bus. + /// + /// - Parameters: + /// - message: The Feature message + /// - core: The core from which the message is transmitted. + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { + guard case var .webview(.log(event)) = message else { + return false + } + + let versionKey = LogEventEncoder.StaticCodingKeys.applicationVersion.rawValue + let envKey = LogEventEncoder.StaticCodingKeys.environment.rawValue + let tagsKey = LogEventEncoder.StaticCodingKeys.tags.rawValue + let dateKey = LogEventEncoder.StaticCodingKeys.date.rawValue + + core.scope(for: LogsFeature.self).eventWriteContext { context, writer in + let ddTags = "\(versionKey):\(context.version),\(envKey):\(context.env)" + + if let tags = event[tagsKey] as? String, !tags.isEmpty { + event[tagsKey] = "\(ddTags),\(tags)" + } else { + event[tagsKey] = ddTags + } + + if let timestampInMs = event[dateKey] as? Int { + let serverTimeOffsetInMs = context.serverTimeOffset.toInt64Milliseconds + let correctedTimestamp = Int64(timestampInMs) + serverTimeOffsetInMs + event[dateKey] = correctedTimestamp + } + + if let rum = context.baggages[RUMContext.key] { + do { + let rum = try rum.decode(type: RUMContext.self) + event[LogEvent.Attributes.RUM.applicationID] = rum.applicationID + event[LogEvent.Attributes.RUM.sessionID] = rum.sessionID + event[LogEvent.Attributes.RUM.viewID] = rum.viewID + event[LogEvent.Attributes.RUM.actionID] = rum.userActionID + } catch { + core.telemetry.error("Fails to decode RUM context from Logs in `WebViewLogReceiver`", error: error) + } + } + + writer.write(value: AnyEncodable(event)) + } + + return true + } +} diff --git a/DatadogLogs/Sources/Feature/RequestBuilder.swift b/DatadogLogs/Sources/Feature/RequestBuilder.swift new file mode 100644 index 0000000000..1444c19425 --- /dev/null +++ b/DatadogLogs/Sources/Feature/RequestBuilder.swift @@ -0,0 +1,62 @@ +/* + * 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 + +/// The Logging URL Request Builder for formatting and configuring the `URLRequest` +/// to upload logs data. +internal struct RequestBuilder: FeatureRequestBuilder { + /// A custom logs intake. + let customIntakeURL: URL? + + /// The logs request body format. + let format = DataFormat(prefix: "[", suffix: "]", separator: ",") + + /// Telemetry interface. + let telemetry: Telemetry + + init( + customIntakeURL: URL? = nil, + telemetry: Telemetry = NOPTelemetry() + ) { + self.customIntakeURL = customIntakeURL + self.telemetry = telemetry + } + + func request( + for events: [Event], + with context: DatadogContext, + execution: ExecutionContext + ) -> URLRequest { + let builder = URLRequestBuilder( + url: url(with: context), + queryItems: [ + .ddsource(source: context.source), + ], + headers: [ + .contentTypeHeader(contentType: .applicationJSON), + .userAgentHeader( + appName: context.applicationName, + appVersion: context.version, + device: context.device + ), + .ddAPIKeyHeader(clientToken: context.clientToken), + .ddEVPOriginHeader(source: context.ciAppOrigin ?? context.source), + .ddEVPOriginVersionHeader(sdkVersion: context.sdkVersion), + .ddRequestIDHeader(), + ], + telemetry: telemetry + ) + + let data = format.format(events.map { $0.data }) + return builder.uploadRequest(with: data) + } + + private func url(with context: DatadogContext) -> URL { + customIntakeURL ?? context.site.endpoint.appendingPathComponent("api/v2/logs") + } +} diff --git a/DatadogLogs/Sources/Log/LogEventBuilder.swift b/DatadogLogs/Sources/Log/LogEventBuilder.swift new file mode 100644 index 0000000000..f582829e07 --- /dev/null +++ b/DatadogLogs/Sources/Log/LogEventBuilder.swift @@ -0,0 +1,140 @@ +/* + * 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 + +/// Builds `LogEvent` from data received from the user and provided internally by the SDK. +internal struct LogEventBuilder { + /// The `service` value for logs. + /// See: [Unified Service Tagging](https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging). + let service: String + /// The `logger.name` value for logs. + let loggerName: String? + /// Whether to send the network info in `network.client.*` log attributes. + let networkInfoEnabled: Bool + /// Allows for modifying (or dropping) logs before they get sent. + let eventMapper: LogEventMapper? + + /// Creates `LogEvent`. + /// + /// To ensure that logs include precise and correct information, some parameters must be collected synchronously on the caller thread + /// whereas other don't. For example, it is important to sign logs with a `date` read exactly from the moment of public API call, but + /// network info and other parts of the SDK `context` can be provided asynchronously. + /// + /// This is to guarantee the right order of logs in Datadog app when using multiple loggers on the same thread and to make sure + /// that reported application context is accurate for the moment of log creation. + /// + /// - Parameters: + /// - date: date of creating the log + /// - level: the severity level of the log + /// - message: the message of the log + /// - error: eventual error to associate with log + /// - errorFingerprint: the custom fingerprint for this log + /// - binaryImages: binary images needed to symbolicate the error + /// - attributes: attributes to associate with log (user and internal attributes, separate) + /// - tags: tags to associate with log + /// - context: SDK context from the moment of creating log + /// - threadName: the name of the thread on which the log is created. + /// - callback: The callback to return the modified `LogEvent`. + /// + /// - Note: `date` and `threadName` must be collected on the user thread. + func createLogEvent( + date: Date, + level: LogLevel, + message: String, + error: DDError?, + errorFingerprint: String?, + binaryImages: [BinaryImage]?, + attributes: LogEvent.Attributes, + tags: Set, + context: DatadogContext, + threadName: String, + callback: @escaping (LogEvent) -> Void + ) { + let userInfo = context.userInfo ?? .empty + + let log = LogEvent( + date: date.addingTimeInterval(context.serverTimeOffset), + status: level.asLogStatus, + message: message, + error: error.map { + .init( + kind: $0.type, + message: $0.message, + stack: $0.stack, + sourceType: $0.sourceType, + fingerprint: errorFingerprint, + binaryImages: binaryImages?.toLogDataFormat + ) + }, + serviceName: service, + environment: context.env, + loggerName: loggerName ?? context.applicationBundleIdentifier, + loggerVersion: context.sdkVersion, + threadName: threadName, + applicationVersion: context.version, + applicationBuildNumber: context.buildNumber, + buildId: context.buildId, + variant: context.variant, + dd: LogEvent.Dd( + device: LogEvent.DeviceInfo( + brand: context.device.brand, + name: context.device.name, + model: context.device.model, + architecture: context.device.architecture + ) + ), + os: .init( + name: context.device.osName, + version: context.device.osVersion, + build: context.device.osBuildNumber + ), + userInfo: .init( + id: userInfo.id, + name: userInfo.name, + email: userInfo.email, + extraInfo: userInfo.extraInfo + ), + networkConnectionInfo: networkInfoEnabled ? context.networkConnectionInfo : nil, + mobileCarrierInfo: networkInfoEnabled ? context.carrierInfo : nil, + attributes: attributes, + tags: !tags.isEmpty ? Array(tags) : nil + ) + + eventMapper?.map(event: log, callback: callback) ?? callback(log) + } +} + +internal extension LogLevel { + var asLogStatus: 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 + } + } +} + +internal extension BinaryImage { + var toLogDataFormat: LogEvent.Error.BinaryImage { + return .init( + arch: architecture, + isSystem: isSystemLibrary, + loadAddress: loadAddress, + maxAddress: maxAddress, + name: libraryName, + uuid: uuid + ) + } +} + +internal extension Array where Element == BinaryImage { + var toLogDataFormat: [LogEvent.Error.BinaryImage] { map { $0.toLogDataFormat } } +} diff --git a/DatadogLogs/Sources/Log/LogEventEncoder.swift b/DatadogLogs/Sources/Log/LogEventEncoder.swift new file mode 100644 index 0000000000..4d130bef22 --- /dev/null +++ b/DatadogLogs/Sources/Log/LogEventEncoder.swift @@ -0,0 +1,354 @@ +/* + * 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 + +/// `Encodable` representation of log. It gets sanitized before encoding. +/// All mutable properties are subject of sanitization. +public struct LogEvent: Encodable { + /// The Log event status definitions. + public enum Status: String, Encodable, CaseIterable, Equatable { + case debug + case info + case notice + case warn + case error + case critical + case emergency + } + + /// Custom attributes associated with a the log event. + public struct Attributes { + /// List of log attribute keys used to establish the link between the Log event and the RUM session that it was collected within. + /// Those keys are recognised by Datadog app and used to render the link in web UI. + internal enum RUM { + /// Key referencing the RUM applicaiton ID. + static let applicationID = "application_id" + /// Key referencing the RUM session ID. + static let sessionID = "session_id" + /// Key referencing the RUM view ID. + static let viewID = "view.id" + /// Key referencing the RUM action ID. + static let actionID = "user_action.id" + } + + /// List of log attribute keys used to establish the link between the Log event and the Tracing span that it was collected within. + /// Those keys are recognised by Datadog app and used to render the link in web UI. + internal enum Trace { + /// Key referencing the trace ID. + static let traceID = "dd.trace_id" + /// Key referencing the span ID. + static let spanID = "dd.span_id" + } + + /// Log custom attributes, They are subject for sanitization. + public var userAttributes: [String: Encodable] + /// Log attributes added internally by the SDK. They are not a subject for sanitization. + internal let internalAttributes: [String: Encodable]? + } + + /// User information associated with a the log event. + public struct UserInfo { + /// User ID, if any. + public let id: String? + /// Name representing the user, if any. + public let name: String? + /// User email, if any. + public let email: String? + /// User custom attributes, if any. + public var extraInfo: [String: Encodable] + } + + /// Error description associated with a log event. + public struct Error { + /// Description of BinaryImage (used for symbolicaiton of stack traces) + public struct BinaryImage: Codable { + /// CPU architecture from the library. + public let arch: String? + + /// Determines if it's a system or user library. + public let isSystem: Bool + + /// Library's load address (hexadecimal). + public let loadAddress: String? + + /// Max value from the library address range (hexadecimal). + public let maxAddress: String? + + /// Name of the library. + public let name: String + + /// Build UUID that uniquely identifies the binary image. + public let uuid: String + + enum CodingKeys: String, CodingKey { + case arch = "arch" + case isSystem = "is_system" + case loadAddress = "load_address" + case maxAddress = "max_address" + case name = "name" + case uuid = "uuid" + } + } + + /// The Log error kind + public var kind: String? + /// The Log error message + public var message: String? + /// The Log error stack + public var stack: String? + /// The Log error source_type. Used by cross platform SDKs + public var sourceType: String = "ios" + /// The custom fingerprint supplied for this error, if any + public var fingerprint: String? + /// Binary images needed to decode the provided stack (if any) + public var binaryImages: [BinaryImage]? + } + + /// Device information. + public struct DeviceInfo: Codable { + /// Device manufacturer name. Always'Apple' + public let brand: String + + /// Device marketing name, e.g. "iPhone", "iPad", "iPod touch". + public let name: String + + /// Device model name, e.g. "iPhone10,1", "iPhone13,2". + public let model: String + + /// The architecture of the device + public let architecture: String + } + + /// Operating System description. + public struct OperatingSystem: Codable { + /// Operating system name, e.g. Android, iOS + public let name: String + /// Full operating system version, e.g. 8.1.1 + public let version: String + /// Operating system build number, e.g. 15D21 + public let build: String? + } + + /// Datadog specific attributes. + public struct Dd: Codable { + /// Device information + public let device: DeviceInfo + } + + /// The log's timestamp + public let date: Date + /// The log status + public let status: Status + /// The log message + public var message: String + /// The associated log error + public var error: Error? + /// The service name configured for Logs. + public let serviceName: String + /// The current log environement. + public let environment: String + /// The configured logger name. + public let loggerName: String + /// The current logger version. + public let loggerVersion: String + /// The thread's name this log event has been sent from. + public let threadName: String? + /// The current application version. + public let applicationVersion: String + /// The current application build number. + public let applicationBuildNumber: String + /// The id of the current build (used for some cross platform frameworks) + public let buildId: String? + /// The variant of the current build (used in some cross platform frameworks) + public let variant: String? + /// Datadog specific attributes + public let dd: Dd + /// The associated log error + public let os: OperatingSystem + /// Custom user information configured globally for the SDK. + public var userInfo: UserInfo + /// The network connection information from the moment the log was sent. + public let networkConnectionInfo: NetworkConnectionInfo? + /// The mobile carrier information from the moment the log was sent. + public let mobileCarrierInfo: CarrierInfo? + /// The attributes associated with this log. + public var attributes: LogEvent.Attributes + /// Tags associated with this log. + public var tags: [String]? + + public func encode(to encoder: Encoder) throws { + let sanitizedLog = LogEventSanitizer().sanitize(log: self) + try LogEventEncoder().encode(sanitizedLog, to: encoder) + } +} + +/// Encodes `Log` to given encoder. +internal struct LogEventEncoder { + /// Coding keys for permanent `Log` attributes. + enum StaticCodingKeys: String, CodingKey { + case date + case status + case message + case serviceName = "service" + case environment = "env" + case tags = "ddtags" + case os = "os" + + // MARK: - Error + + case errorKind = "error.kind" + case errorMessage = "error.message" + case errorStack = "error.stack" + case errorSourceType = "error.source_type" + case errorFingerprint = "error.fingerprint" + case errorBinaryImages = "error.binary_images" + + // MARK: - Application info + + case applicationVersion = "version" + case applicationBuildNumber = "build_version" + case buildId = "build_id" + + // MARK: - Dd info + case dd = "_dd" + + // MARK: - Logger info + + case loggerName = "logger.name" + case loggerVersion = "logger.version" + case threadName = "logger.thread_name" + + // MARK: - User info + + case userId = "usr.id" + case userName = "usr.name" + case userEmail = "usr.email" + + // MARK: - Network connection info + + case networkReachability = "network.client.reachability" + case networkAvailableInterfaces = "network.client.available_interfaces" + case networkConnectionSupportsIPv4 = "network.client.supports_ipv4" + case networkConnectionSupportsIPv6 = "network.client.supports_ipv6" + case networkConnectionIsExpensive = "network.client.is_expensive" + case networkConnectionIsConstrained = "network.client.is_constrained" + + // MARK: - Mobile carrier info + + case mobileNetworkCarrierName = "network.client.sim_carrier.name" + case mobileNetworkCarrierISOCountryCode = "network.client.sim_carrier.iso_country" + case mobileNetworkCarrierRadioTechnology = "network.client.sim_carrier.technology" + case mobileNetworkCarrierAllowsVoIP = "network.client.sim_carrier.allows_voip" + } + + /// Coding keys for dynamic `Log` attributes specified by user. + private struct DynamicCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + init?(stringValue: String) { self.stringValue = stringValue } + init?(intValue: Int) { return nil } + init(_ string: String) { self.stringValue = string } + } + + func encode(_ log: LogEvent, to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StaticCodingKeys.self) + try container.encode(log.date, forKey: .date) + try container.encode(log.status, forKey: .status) + try container.encode(log.message, forKey: .message) + try container.encode(log.serviceName, forKey: .serviceName) + try container.encode(log.os, forKey: .os) + + // Encode log.error properties + if let someError = log.error { + try container.encode(someError.kind, forKey: .errorKind) + try container.encode(someError.message, forKey: .errorMessage) + try container.encode(someError.stack, forKey: .errorStack) + try container.encode(someError.sourceType, forKey: .errorSourceType) + try container.encode(someError.fingerprint, forKey: .errorFingerprint) + if let binaryImages = someError.binaryImages { + try container.encode(binaryImages, forKey: .errorBinaryImages) + } + } + + // Encode logger info + try container.encode(log.loggerName, forKey: .loggerName) + try container.encode(log.loggerVersion, forKey: .loggerVersion) + try container.encode(log.threadName, forKey: .threadName) + + // Encode application info + try container.encode(log.applicationVersion, forKey: .applicationVersion) + try container.encode(log.applicationBuildNumber, forKey: .applicationBuildNumber) + if let buildId = log.buildId { + try container.encode(buildId, forKey: .buildId) + } + + try container.encode(log.dd, forKey: .dd) + + // Encode user info + try log.userInfo.id.ifNotNil { try container.encode($0, forKey: .userId) } + try log.userInfo.name.ifNotNil { try container.encode($0, forKey: .userName) } + try log.userInfo.email.ifNotNil { try container.encode($0, forKey: .userEmail) } + + // Encode network info + if let networkConnectionInfo = log.networkConnectionInfo { + try container.encode(networkConnectionInfo.reachability, forKey: .networkReachability) + try container.encode(networkConnectionInfo.availableInterfaces, forKey: .networkAvailableInterfaces) + try container.encode(networkConnectionInfo.supportsIPv4, forKey: .networkConnectionSupportsIPv4) + try container.encode(networkConnectionInfo.supportsIPv6, forKey: .networkConnectionSupportsIPv6) + try container.encode(networkConnectionInfo.isExpensive, forKey: .networkConnectionIsExpensive) + try networkConnectionInfo.isConstrained.ifNotNil { + try container.encode($0, forKey: .networkConnectionIsConstrained) + } + } + + // Encode mobile carrier info + if let carrierInfo = log.mobileCarrierInfo { + try carrierInfo.carrierName.ifNotNil { + try container.encode($0, forKey: .mobileNetworkCarrierName) + } + try carrierInfo.carrierISOCountryCode.ifNotNil { + try container.encode($0, forKey: .mobileNetworkCarrierISOCountryCode) + } + try container.encode(carrierInfo.radioAccessTechnology, forKey: .mobileNetworkCarrierRadioTechnology) + try container.encode(carrierInfo.carrierAllowsVOIP, forKey: .mobileNetworkCarrierAllowsVoIP) + } + + // Encode attributes... + var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self) + + // 1. user info attributes + try log.userInfo.extraInfo.forEach { + let key = DynamicCodingKey("usr.\($0)") + try attributesContainer.encode(AnyEncodable($1), forKey: key) + } + + // 2. user attributes + let encodableUserAttributes = Dictionary( + uniqueKeysWithValues: log.attributes.userAttributes.map { name, value in (name, AnyEncodable(value)) } + ) + try encodableUserAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } + + // 3. internal attributes + if let internalAttributes = log.attributes.internalAttributes { + let encodableInternalAttributes = Dictionary( + uniqueKeysWithValues: internalAttributes.map { name, value in (name, AnyEncodable(value)) } + ) + try encodableInternalAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } + } + + // Encode tags + var tags = log.tags ?? [] + tags.append("env:\(log.environment)") // include default env tag + tags.append("version:\(log.applicationVersion)") // include default version tag + if let variant = log.variant { + tags.append("variant:\(variant)") + } + let tagsString = tags.joined(separator: ",") + try container.encode(tagsString, forKey: .tags) + } +} diff --git a/DatadogLogs/Sources/Log/LogEventSanitizer.swift b/DatadogLogs/Sources/Log/LogEventSanitizer.swift new file mode 100644 index 0000000000..58f89aedf8 --- /dev/null +++ b/DatadogLogs/Sources/Log/LogEventSanitizer.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. + */ + +import Foundation +import DatadogInternal + +/// Sanitizes `Log` representation received from the user, so it can match Datadog log constraints. +internal struct LogEventSanitizer { + internal struct Constraints { + /// Attribute names reserved for Datadog. + /// If any of those is used by the user, the attribute will be ignored. + static let reservedAttributeNames: Set = [ + "host", "message", "status", "service", "source", "ddtags", + "dd.trace_id", "dd.span_id", + "application_id", "session_id", "view.id", "user_action.id", + "build_id", + ] + /// Allowed first character of a tag name (given as ASCII values ranging from lowercased `a` to `z`) . + /// Tags with name starting with different character will be dropped. + static let allowedTagNameFirstCharacterASCIIRange: [UInt8] = Array(97...122) + /// Maximum length of the tag. + /// Tags exceeting this length will be trunkated. + static let maxTagLength: Int = 200 + /// Tag keys reserved for Datadog. + /// If any of those is used by user, the tag will be ignored. + static let reservedTagKeys: Set = [ + "host", "device", "source", "service", "env" + ] + /// Maximum number of attributes in log. + /// If this number is exceeded, extra attributes will be ignored. + static let maxNumberOfTags: Int = 100 + } + + private let attributesSanitizer = AttributesSanitizer(featureName: "Log") + + func sanitize(log: LogEvent) -> LogEvent { + let sanitizedAttributes = sanitize(attributes: log.attributes) + let sanitizedTags = sanitize(tags: log.tags) + + var sanitizedLog = log + sanitizedLog.attributes = sanitizedAttributes + sanitizedLog.tags = sanitizedTags + return sanitizedLog + } + + // MARK: - Attributes sanitization + + private func sanitize(attributes rawAttributes: LogEvent.Attributes) -> LogEvent.Attributes { + // Sanitizes only `userAttributes`, `internalAttributes` remain untouched + var userAttributes = rawAttributes.userAttributes + userAttributes = removeInvalidAttributes(userAttributes) + userAttributes = removeReservedAttributes(userAttributes) + userAttributes = attributesSanitizer.sanitizeKeys(for: userAttributes) + let userAttributesLimit = AttributesSanitizer.Constraints.maxNumberOfAttributes - (rawAttributes.internalAttributes?.count ?? 0) + userAttributes = attributesSanitizer.limitNumberOf(attributes: userAttributes, to: userAttributesLimit) + + return LogEvent.Attributes( + userAttributes: userAttributes, + internalAttributes: rawAttributes.internalAttributes + ) + } + + private func removeInvalidAttributes(_ attributes: [String: Encodable]) -> [String: Encodable] { + // Attribute name cannot be empty + return attributes.filter { attribute in + if attribute.key.isEmpty { + DD.logger.error("Attribute key is empty. This attribute will be ignored.") + return false + } + return true + } + } + + private func removeReservedAttributes(_ attributes: [String: Encodable]) -> [String: Encodable] { + return attributes.filter { attribute in + if Constraints.reservedAttributeNames.contains(attribute.key) { + DD.logger.error("'\(attribute.key)' is a reserved attribute name. This attribute will be ignored.") + return false + } + return true + } + } + + // MARK: - Tags sanitization + + private func sanitize(tags rawTags: [String]?) -> [String]? { + if let rawTags = rawTags { + let tags = rawTags + .map { $0.lowercased() } + .filter { startsWithAllowedCharacter(tag: $0) } + .map { replaceIllegalCharactersIn(tag: $0) } + .map { removeTrailingCommasIn(tag: $0) } + .map { limitToMaxLength(tag: $0) } + .filter { isNotReserved(tag: $0) } + return limitToMaxNumberOfTags(tags) + } else { + return nil + } + } + + private func startsWithAllowedCharacter(tag: String) -> Bool { + guard let firstCharacter = tag.first?.asciiValue else { + DD.logger.error("Tag is empty and will be ignored.") + return false + } + + // Tag must start with a letter + if Constraints.allowedTagNameFirstCharacterASCIIRange.contains(firstCharacter) { + return true + } else { + DD.logger.error("Tag '\(tag)' starts with an invalid character and will be ignored.") + return false + } + } + + private func replaceIllegalCharactersIn(tag: String) -> String { + let sanitized = tag.replacingOccurrences(of: #"[^a-z0-9_:.\/-]"#, with: "_", options: .regularExpression) + if sanitized != tag { + DD.logger.warn("Tag '\(tag)' was modified to '\(sanitized)' to match Datadog constraints.") + } + return sanitized + } + + private func removeTrailingCommasIn(tag: String) -> String { + // If present, remove trailing commas `:` + var sanitized = tag + while sanitized.last == ":" { _ = sanitized.removeLast() } + if sanitized != tag { + DD.logger.warn("Tag '\(tag)' was modified to '\(sanitized)' to match Datadog constraints.") + } + return sanitized + } + + private func limitToMaxLength(tag: String) -> String { + if tag.count > Constraints.maxTagLength { + let sanitized = String(tag.prefix(Constraints.maxTagLength)) + DD.logger.warn("Tag '\(tag)' was modified to '\(sanitized)' to match Datadog constraints.") + return sanitized + } else { + return tag + } + } + + private func isNotReserved(tag: String) -> Bool { + if let colonIndex = tag.firstIndex(of: ":") { + let key = String(tag.prefix(upTo: colonIndex)) + if Constraints.reservedTagKeys.contains(key) { + DD.logger.warn("'\(key)' is a reserved tag key. This tag will be ignored.") + return false + } else { + return true + } + } else { + return true + } + } + + private func limitToMaxNumberOfTags(_ tags: [String]) -> [String] { + // Only `Constraints.maxNumberOfTags` of tags are allowed. + if tags.count > Constraints.maxNumberOfTags { + let extraTagsCount = tags.count - Constraints.maxNumberOfTags + DD.logger.warn("Number of tags exceeds the limit of \(Constraints.maxNumberOfTags). \(extraTagsCount) attribute(s) will be ignored.") + return tags.dropLast(extraTagsCount) + } else { + return tags + } + } +} 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 new file mode 100644 index 0000000000..c50da3dc01 --- /dev/null +++ b/DatadogLogs/Sources/Logger.swift @@ -0,0 +1,203 @@ +/* + * 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 + +/// Datadog logger. +public struct Logger { + public struct Configuration { + /// Format to use when printing logs to console. + public enum ConsoleLogFormat { + /// Prints short representation of log. + case short + /// Prints short representation of log with given prefix. + case shortWith(prefix: String) + } + + /// The service name (default value is set to application bundle identifier) + public var service: String? + + /// The logger custom name (default value is set to main bundle identifier) + public var name: String? + + /// Enriches logs with network connection info. + /// + /// This means: reachability status, connection type, mobile carrier name and many more will be added to each log. + /// For full list of network info attributes see `NetworkConnectionInfo` and `CarrierInfo`. + /// + /// `false` by default. + public var networkInfoEnabled: Bool + + /// Enables the logs integration with RUM. + /// + /// If enabled all the logs will be enriched with the current RUM View information and + /// it will be possible to see all the logs sent during a specific View lifespan in the RUM Explorer. + /// + /// `true` by default. + public var bundleWithRumEnabled: Bool + + /// Enables the logs integration with active span API from Tracing. + /// + /// If enabled all the logs will be bundled with the `DatadogTracer.shared().activeSpan` trace and + /// it will be possible to see all the logs sent during that specific trace. + /// + /// `true` by default. + public var bundleWithTraceEnabled: Bool + + /// Sets the sample rate for remote logging. + /// + /// **When set to `0`, no log entries will be sent to Datadog servers.** + /// A value of`100` means all logs will be processed. + /// + /// When setting the `remoteSampleRate` to `0` + /// + /// Default is `100`, meaning that all logs will be sent. + public var remoteSampleRate: Float + + /// Set the minimum log level reported to Datadog servers. + /// Any log with a level equal or above the threshold will be sent. + /// + /// Note: this setting doesn't impact logs printed to the console if `printLogsToConsole(_:)` + /// is used - all logs will be printed, no matter of their level. + /// + /// `LogLevel.debug` by default + public var remoteLogThreshold: LogLevel + + /// Format to use when printing logs to console - either `.short` or `.json`. + /// + /// Do not print to console by default. + public var consoleLogFormat: ConsoleLogFormat? + + /// Overrides the current process info. + internal var processInfo: ProcessInfo = .processInfo + + /// Creates a Logger Configuration. + /// + /// - Parameters: + /// - service: The service name (default value is set to application bundle identifier) + /// - name: The logger custom name (default value is set to main bundle identifier) + /// - networkInfoEnabled: Enriches logs with network connection info. `false` by default. + /// - bundleWithRUM: Enables the logs integration with RUM. `true` by default. + /// - bundleWithTraceEnabled: Enables the logs integration with active span API from Tracing. `true` by default + /// - remoteSampleRate: The sample rate for remote logging. **When set to `0`, no log entries will be sent to Datadog servers.** + /// - remoteLogThreshold: Set the minimum log level reported to Datadog servers. .debug by default. + /// - consoleLogFormat: Format to use when printing logs to console - either `.short` or `.json`. + public init( + service: String? = nil, + name: String? = nil, + networkInfoEnabled: Bool = false, + bundleWithRumEnabled: Bool = true, + bundleWithTraceEnabled: Bool = true, + remoteSampleRate: Float = .maxSampleRate, + remoteLogThreshold: LogLevel = .debug, + consoleLogFormat: ConsoleLogFormat? = nil + ) { + self.service = service + self.name = name + self.networkInfoEnabled = networkInfoEnabled + self.bundleWithRumEnabled = bundleWithRumEnabled + self.bundleWithTraceEnabled = bundleWithTraceEnabled + self.remoteSampleRate = remoteSampleRate + self.remoteLogThreshold = remoteLogThreshold + self.consoleLogFormat = consoleLogFormat + } + } + + // MARK: - Logger Creation + + /// Creates a Logger complying with `LoggerProtocol`. + /// + /// - Parameters: + /// - configuration: The logger configuration. + /// - core: The instance of Datadog SDK to enable Logs in (global instance by default). + /// - Returns: A logger instance. + public static func create( + with configuration: Configuration = .init(), + in core: DatadogCoreProtocol = CoreRegistry.default + ) -> LoggerProtocol { + do { + return try createOrThrow(with: configuration, in: core) + } catch { + DD.logger.critical("Failed to build `Logger`.", error: error) + return NOPLogger() + } + } + + /// Creates a Logger complying with `LoggerProtocol` or throw an error. + /// + /// - Parameters: + /// - configuration: The logger configuration. + /// - core: The instance of Datadog SDK to enable Logs in (global instance by default). + /// - Returns: A logger instance. + 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.create()`." + ) + } + + guard let feature = core.get(feature: LogsFeature.self) else { + throw ProgrammerError( + description: "`Logger.create()` produces a non-functional logger because the `Logs` feature was not enabled." + ) + } + + let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) + + let remoteLogger: RemoteLogger? = { + guard configuration.remoteSampleRate > 0 else { + return nil + } + + return RemoteLogger( + featureScope: core.scope(for: LogsFeature.self), + globalAttributes: feature.attributes, + configuration: RemoteLogger.Configuration( + service: configuration.service, + name: configuration.name, + networkInfoEnabled: configuration.networkInfoEnabled, + threshold: configuration.remoteLogThreshold, + eventMapper: feature.logEventMapper, + sampler: Sampler(samplingRate: debug ? 100 : configuration.remoteSampleRate) + ), + dateProvider: feature.dateProvider, + rumContextIntegration: configuration.bundleWithRumEnabled, + activeSpanIntegration: configuration.bundleWithTraceEnabled, + backtraceReporter: feature.backtraceReporter + ) + }() + + let consoleLogger: ConsoleLogger? = { + guard let consoleLogFormat = configuration.consoleLogFormat else { + return nil + } + + return ConsoleLogger( + configuration: ConsoleLogger.Configuration( + timeZone: .current, + format: consoleLogFormat + ), + dateProvider: feature.dateProvider, + printFunction: consolePrint + ) + }() + + switch (remoteLogger, consoleLogger) { + case (let remoteLogger?, nil): + return remoteLogger + + case (nil, let consoleLogger?): + return consoleLogger + + case (let remoteLogger?, let consoleLogger?): + return CombinedLogger(combinedLoggers: [remoteLogger, consoleLogger]) + + case (nil, nil): // when user explicitly produces a no-op logger + return NOPLogger() + } + } +} diff --git a/DatadogLogs/Sources/LoggerProtocol+Internal.swift b/DatadogLogs/Sources/LoggerProtocol+Internal.swift new file mode 100644 index 0000000000..a379767d44 --- /dev/null +++ b/DatadogLogs/Sources/LoggerProtocol+Internal.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 Foundation +import DatadogInternal + +/// Internal Logger for Cross-Platform access. +public protocol InternalLoggerProtocol { + /// General purpose logging method. + /// Sends a log with certain `level`, `message`, `errorKind`, `errorMessage`, `stackTrace` and `attributes`. + /// + /// This method is meant for non-native or cross platform frameworks (such as React Native or Flutter) to send error information + /// to Datadog. Although it can be used directly, it is recommended to use other methods declared on `Logger`. + /// + /// - Parameters: + /// - level: the log level + /// - message: the message to be logged + /// - errorKind: the kind of error reported + /// - errorMessage: the message attached to the error + /// - stackTrace: a string representation of the error's stack trace + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func log( + level: LogLevel, + message: String, + errorKind: String?, + errorMessage: String?, + stackTrace: String?, + attributes: [String: Encodable]? + ) +} + +private struct NOPInternalLogger: InternalLoggerProtocol { + func log( + level: LogLevel, + message: String, + errorKind: String?, + errorMessage: String?, + stackTrace: String?, + attributes: [String: Encodable]? + ) { } +} + +/// Extends `LoggerProtocol` with additional methods designed for Datadog cross-platform SDKs. +extension LoggerProtocol { + /// Grants access to an internal interface utilized only by Datadog cross-platform SDKs. + /// **It is not meant for public use** and it might change without prior notice. + public var _internal: InternalLoggerProtocol { + self as? InternalLoggerProtocol ?? NOPInternalLogger() + } +} diff --git a/DatadogLogs/Sources/LoggerProtocol.swift b/DatadogLogs/Sources/LoggerProtocol.swift new file mode 100644 index 0000000000..07c28adda0 --- /dev/null +++ b/DatadogLogs/Sources/LoggerProtocol.swift @@ -0,0 +1,242 @@ +/* + * 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 + +/// Log levels ordered by their severity, with `.debug` being the least severe and +/// `.critical` being the most severe. +public enum LogLevel: Int, Codable { + case debug + case info + case notice + case warn + case error + case critical +} + +extension CoreLoggerLevel { + public init(logLevel: LogLevel) { + switch logLevel { + case .debug, .info, .notice: self = .debug + case .warn: self = .warn + case .error: self = .error + case .critical: self = .critical + } + } +} + +/// Datadog Logger. +/// +/// Usage: +/// +/// import DatadogLogs +/// +/// // Initialise the Logs module +/// +/// // logger reference +/// var logger = Logger.create() +public protocol LoggerProtocol: Sendable { + /// General purpose logging method. + /// Sends a log with certain `level`, `message`, `error` and `attributes`. + /// + /// Although it can be used directly, it is more convenient and recommended to use specific methods declared on `Logger`: + /// * `debug(_:error:attributes:)` + /// * `info(_:error:attributes:)` + /// * ... + /// + /// - Parameters: + /// - level: the log level + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) + + // MARK: - Attributes + + /// Adds a custom attribute to all future logs sent by this logger. + /// - Parameters: + /// - key: the attribute key. See `AttributeKey` documentation for information on nesting attributes with dot `.` syntax. + /// - value: the attribute value that conforms to `Encodable`. See `AttributeValue` documentation + /// for information on nested encoding containers limitation. + func addAttribute(forKey key: AttributeKey, value: AttributeValue) + + /// Removes the custom attribute from all future logs sent by this logger. + /// + /// Previous logs won't lose this attribute if sent prior to this call. + /// - Parameter key: the key of an attribute that will be removed. + func removeAttribute(forKey key: AttributeKey) + + // MARK: - Tags + + /// Adds a `"key:value"` tag to all future logs sent by this logger. + /// + /// Tags must start with a letter and + /// * may contain: alphanumerics, underscores, minuses, colons, periods and slashes; + /// * other special characters are converted to underscores; + /// * must be lowercase + /// * and can be at most 200 characters long (tags exceeding this limit will be truncated to first 200 characters). + /// + /// See also: [Defining Tags](https://docs.datadoghq.com/tagging/#defining-tags) + /// + /// - Parameter key: tag key + /// - Parameter value: tag value + func addTag(withKey key: String, value: String) + + /// Remove all tags with the given key from all future logs sent by this logger. + /// + /// Previous logs won't lose this tag if created prior to this call. + /// + /// - Parameter key: the key of the tag to remove + func removeTag(withKey key: String) + + /// Adds the tag to all future logs sent by this logger. + /// + /// Tags must start with a letter and + /// * may contain: alphanumerics, underscores, minuses, colons, periods and slashes; + /// * other special characters are converted to underscores; + /// * must be lowercase + /// * and can be at most 200 characters long (tags exceeding this limit will be truncated to first 200 characters). + /// + /// See also: [Defining Tags](https://docs.datadoghq.com/tagging/#defining-tags) + /// + /// - Parameter tag: value of the tag + func add(tag: String) + + /// Removes the tag from all future logs sent by this logger. + /// + /// Previous logs won't lose the this tag if created prior to this call. + /// + /// - Parameter tag: the value of the tag to remove + func remove(tag: String) +} + +public extension LoggerProtocol { + /// Sends a DEBUG log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func debug(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .debug, message: message, error: error, attributes: attributes) + } + + /// Sends an INFO log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func info(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .info, message: message, error: error, attributes: attributes) + } + + /// Sends a NOTICE log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func notice(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .notice, message: message, error: error, attributes: attributes) + } + + /// Sends a WARN log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func warn(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .warn, message: message, error: error, attributes: attributes) + } + + /// Sends an ERROR log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func error(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .error, message: message, error: error, attributes: attributes) + } + + /// Sends a CRITICAL log message. + /// - Parameters: + /// - message: the message to be logged + /// - error: the error information (optional) + /// - attributes: a dictionary of attributes (optional) to add for this message. If an attribute with + /// the same key already exist in this logger, it will be overridden (only for this message). + func critical(_ message: String, error: Error? = nil, attributes: [AttributeKey: AttributeValue]? = nil) { + log(level: .critical, message: message, error: error, attributes: attributes) + } +} + +internal struct NOPLogger: LoggerProtocol { + func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) {} + func addAttribute(forKey key: AttributeKey, value: AttributeValue) {} + func removeAttribute(forKey key: AttributeKey) {} + func addTag(withKey key: String, value: String) {} + func removeTag(withKey key: String) {} + func add(tag: String) {} + func remove(tag: String) {} +} + +/// Combines multiple loggers together into single `LoggerProtocol` interface. +internal struct CombinedLogger: LoggerProtocol { + let combinedLoggers: [LoggerProtocol] + + func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) { + combinedLoggers.forEach { $0.log(level: level, message: message, error: error, attributes: attributes) } + } + + func addAttribute(forKey key: AttributeKey, value: AttributeValue) { + combinedLoggers.forEach { $0.addAttribute(forKey: key, value: value) } + } + + func removeAttribute(forKey key: AttributeKey) { + combinedLoggers.forEach { $0.removeAttribute(forKey: key) } + } + + func addTag(withKey key: String, value: String) { + combinedLoggers.forEach { $0.addTag(withKey: key, value: value) } + } + + func removeTag(withKey key: String) { + combinedLoggers.forEach { $0.removeTag(withKey: key) } + } + + func add(tag: String) { + combinedLoggers.forEach { $0.add(tag: tag) } + } + + func remove(tag: String) { + combinedLoggers.forEach { $0.remove(tag: tag) } + } +} + +extension CombinedLogger: InternalLoggerProtocol { + func log( + level: LogLevel, + message: String, + errorKind: String?, + errorMessage: String?, + stackTrace: String?, + attributes: [String: Encodable]?) { + combinedLoggers.forEach { + $0._internal.log( + level: level, + message: message, + errorKind: errorKind, + errorMessage: errorMessage, + stackTrace: stackTrace, + attributes: attributes + ) + } + } +} diff --git a/DatadogLogs/Sources/Logs+Internal.swift b/DatadogLogs/Sources/Logs+Internal.swift new file mode 100644 index 0000000000..3d0b25f8fc --- /dev/null +++ b/DatadogLogs/Sources/Logs+Internal.swift @@ -0,0 +1,22 @@ +/* + * 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 2023-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +extension Logs: InternalExtended {} + +extension InternalExtension where ExtendedType == Logs { + /// Check whether `Logs` has been enabled for a specific SDK instance. + /// + /// - Parameters: + /// - in: the core to check + /// + /// - Returns: true if `Logs` has been enabled for the supplied core. + public static func isEnabled(in core: DatadogCoreProtocol = CoreRegistry.default) -> Bool { + return core.get(feature: LogsFeature.self) != nil + } +} diff --git a/DatadogLogs/Sources/Logs.swift b/DatadogLogs/Sources/Logs.swift new file mode 100644 index 0000000000..19534e6ab4 --- /dev/null +++ b/DatadogLogs/Sources/Logs.swift @@ -0,0 +1,133 @@ +/* + * 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 + +/// iOS Log Collection +/// +/// Send logs to Datadog from your iOS applications with Datadog’s dd-sdk-ios client-side logging library and leverage the following features: +/// - Log to Datadog in JSON format natively. +/// - Use default and add custom attributes to each log sent. +/// - Record real client IP addresses and User-Agents. +/// - Leverage optimized network usage with automatic bulk posts. +public enum Logs { + /// The Logs general configuration. + /// + /// This configuration will be applied to all Logger instances. + public struct Configuration { + public typealias EventMapper = (LogEvent) -> LogEvent? + /// Sets the custom mapper for `LogEvent`. This can be used to modify logs before they are send to Datadog. + /// + /// The implementation should obtain a mutable version of the `LogEvent`, modify it and return it. Returning `nil` will result + /// with dropping the Log event entirely, so it won't be send to Datadog. + public var eventMapper: EventMapper? + + /// Overrides the custom server endpoint where Logs are sent. + public var customEndpoint: URL? + + /// Overrides the date provider. + internal var dateProvider: DateProvider = SystemDateProvider() + + /// Overrides the event mapper + internal var _internalEventMapper: LogEventMapper? = nil + + /// Creates a Logs configuration object. + /// + /// - Parameters: + /// - eventMapper: The custom mapper for `LogEvent`. This can be used to modify logs before they are send to Datadog. + /// - customEndpoint: Overrides the custom server endpoint where Logs are sent. + public init( + eventMapper: EventMapper? = nil, + customEndpoint: URL? = nil + ) { + self.eventMapper = eventMapper + self.customEndpoint = customEndpoint + } + } + + /// Enables the Datadog Logs feature. + /// + /// - Parameters: + /// - configuration: The Logs configuration. + /// - core: The instance of Datadog SDK to enable Logs in (global instance by default). + public static func enable( + with configuration: Configuration = .init(), + in core: DatadogCoreProtocol = CoreRegistry.default + ) { + let logEventMapper = configuration._internalEventMapper ?? configuration.eventMapper.map(SyncLogEventMapper.init) + + let feature = LogsFeature( + logEventMapper: logEventMapper, + dateProvider: configuration.dateProvider, + customIntakeURL: configuration.customEndpoint, + telemetry: core.telemetry, + backtraceReporter: core.backtraceReporter + ) + + do { + try core.register(feature: feature) + } catch { + consolePrint("\(error)", .error) + } + } + + /// Adds a custom attribute to all future logs sent by any logger created from the provided Core. + /// - Parameters: + /// - key: the attribute key. See `AttributeKey` documentation for information on nesting attributes with dot `.` syntax. + /// - value: the attribute value that conforms to `Encodable`. See `AttributeValue` documentation + /// for information on nested encoding containers limitation. + /// - core: the `DatadogCoreProtocol` to add the attribute to. + public static func addAttribute(forKey key: AttributeKey, value: AttributeValue, in core: DatadogCoreProtocol = CoreRegistry.default) { + guard let feature = core.get(feature: LogsFeature.self) else { + return + } + feature.attributes.addAttribute(key: key, value: value) + sendAttributesChanged(for: feature, in: core) + } + + /// Removes the custom attribute from all future logs sent any logger created from the provided Core. + /// + /// Previous logs won't lose this attribute if sent prior to this call. + /// - Parameters: + /// - key: the key of an attribute that will be removed. + /// - core: the `DatadogCoreProtocol` to remove the attribute from. + public static func removeAttribute(forKey key: AttributeKey, in core: DatadogCoreProtocol = CoreRegistry.default) { + guard let feature = core.get(feature: LogsFeature.self) else { + return + } + feature.attributes.removeAttribute(forKey: key) + sendAttributesChanged(for: feature, in: core) + } + + private static func sendAttributesChanged(for feature: LogsFeature, in core: DatadogCoreProtocol) { + core.send( + message: .baggage( + key: GlobalLogAttributes.key, + value: GlobalLogAttributes(attributes: feature.attributes.getAttributes()) + ) + ) + } +} + +extension Logs { + /// Attributes that can be added to logs that have special properies in Datadog. + public struct Attributes { + /// Add a custom fingerprint to the error in this log. Requires that the log is supplied with an Error. + /// The value of this attribute must be a `String`. + public static let errorFingerprint = "_dd.error.fingerprint" + } +} + +extension Logs.Configuration: InternalExtended { } +extension InternalExtension where ExtendedType == Logs.Configuration { + /// Sets the custom mapper for `LogEvent`. This can be used to modify logs before they are sent to Datadog. + /// + /// - Parameter mapper: the mapper taking `LogEvent` as input and invoke callback closure with modifier `LogEvent`. + public mutating func setLogEventMapper(_ mapper: LogEventMapper) { + type._internalEventMapper = mapper + } +} diff --git a/DatadogLogs/Sources/RemoteLogger.swift b/DatadogLogs/Sources/RemoteLogger.swift new file mode 100644 index 0000000000..308f9a1077 --- /dev/null +++ b/DatadogLogs/Sources/RemoteLogger.swift @@ -0,0 +1,242 @@ +/* + * 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 + +/// `Logger` sending logs to Datadog. +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? + /// The `logger.name` value for logs. + let name: String? + /// Whether to send the network info in `network.client.*` log attributes. + let networkInfoEnabled: Bool + /// Only logs equal or above this threshold will be sent. + let threshold: LogLevel + /// Allows for modifying (or dropping) logs before they get sent. + let eventMapper: LogEventMapper? + /// Sampler for remote logger. Default is using `100.0` sampling rate. + let sampler: Sampler + } + + /// Logs feature scope. + let featureScope: FeatureScope + /// Configuration specific to this logger. + 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`. + /// Can be `false` if the integration is disabled for this logger. + internal let rumContextIntegration: Bool + /// 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. + private let loggerAttributes: SynchronizedAttributes + /// Logger-specific tags. + private let loggerTags: SynchronizedTags + /// Backtrace reporter for attaching binary images to cross-platform errors. + private let backtraceReporter: BacktraceReporting? + + init( + featureScope: FeatureScope, + globalAttributes: SynchronizedAttributes, + configuration: Configuration, + dateProvider: DateProvider, + rumContextIntegration: Bool, + activeSpanIntegration: Bool, + backtraceReporter: BacktraceReporting? + ) { + 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) { + loggerAttributes.addAttribute(key: key, value: value) + } + + func removeAttribute(forKey key: AttributeKey) { + loggerAttributes.removeAttribute(forKey: key) + } + + // MARK: - Tags + + func addTag(withKey key: String, value: String) { + loggerTags.addTag("\(key):\(value)") + } + + func removeTag(withKey key: String) { + loggerTags.removeTags(where: { $0.hasPrefix("\(key):") }) + } + + func add(tag: String) { + loggerTags.addTag(tag) + } + + func remove(tag: String) { + loggerTags.removeTag(tag) + } + + // MARK: - Logging + + func log(level: LogLevel, message: String, error: Error?, attributes: [String: Encodable]?) { + internalLog(level: level, message: message, error: error.map { DDError(error: $0) }, attributes: attributes) + } + + func internalLog(level: LogLevel, message: String, error: DDError?, attributes: [String: Encodable]?) { + guard configuration.sampler.sample() else { + return + } + guard level.rawValue >= configuration.threshold.rawValue else { + return + } + + // 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 = loggerTags.getTags() + let globalAttributes = globalAttributes.getAttributes() + let loggerAttributes = loggerAttributes.getAttributes() + var logAttributes = attributes + + 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. + 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): + if self.rumContextIntegration, let rum = context.baggages[RUMContext.key] { + do { + let rum = try rum.decode(type: RUMContext.self) + internalAttributes[LogEvent.Attributes.RUM.applicationID] = rum.applicationID + internalAttributes[LogEvent.Attributes.RUM.sessionID] = rum.sessionID + internalAttributes[LogEvent.Attributes.RUM.viewID] = rum.viewID + internalAttributes[LogEvent.Attributes.RUM.actionID] = rum.userActionID + } catch { + self.featureScope.telemetry + .error("Fails to decode RUM context from Logs", error: error) + } + } + + // When bundle with Trace is enabled, link RUM context (if available): + if self.activeSpanIntegration, let spanContext = context.baggages[SpanContext.key] { + do { + let trace = try spanContext.decode(type: SpanContext.self) + internalAttributes[LogEvent.Attributes.Trace.traceID] = trace.traceID?.toString(representation: .hexadecimal) + internalAttributes[LogEvent.Attributes.Trace.spanID] = trace.spanID?.toString(representation: .decimal) + } catch { + self.featureScope.telemetry + .error("Fails to decode Span context from Logs", error: error) + } + } + + // When binary images are requested, add them + var binaryImages: [BinaryImage]? + if addBinaryImages { + // TODO: RUM-4072 Replace full backtrace reporter with simpler binary image fetcher + binaryImages = try? self.backtraceReporter?.generateBacktrace()?.binaryImages + } + + let builder = LogEventBuilder( + service: self.configuration.service ?? context.service, + loggerName: self.configuration.name, + networkInfoEnabled: self.configuration.networkInfoEnabled, + eventMapper: self.configuration.eventMapper + ) + + builder.createLogEvent( + date: date, + level: level, + message: message, + error: error, + errorFingerprint: errorFingerprint, + binaryImages: binaryImages, + attributes: .init( + userAttributes: combinedAttributes, + internalAttributes: internalAttributes + ), + tags: tags, + context: context, + threadName: threadName + ) { log in + writer.write(value: log) + + guard (log.status == .error || log.status == .critical) && !isCrash else { + return + } + + // 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( + time: date, + message: log.error?.message ?? log.message, + type: log.error?.kind, + stack: log.error?.stack, + attributes: .init(busCombinedAttributes), + binaryImages: binaryImages + ) + ) + ) + } + } + } +} + +extension RemoteLogger: InternalLoggerProtocol { + func log(level: LogLevel, message: String, errorKind: String?, errorMessage: String?, stackTrace: String?, attributes: [String: Encodable]?) { + var ddError: DDError? + // Find and remove source_type if it's in the attributes + var logAttributes = attributes + let sourceType = logAttributes?.removeValue(forKey: CrossPlatformAttributes.errorSourceType) as? String + + if errorKind != nil || errorMessage != nil || stackTrace != nil { + // Cross platform frameworks don't necessarilly send all values for errors. Send empty strings + // for any values that are empty. + ddError = DDError(type: errorKind ?? "", message: errorMessage ?? "", stack: stackTrace ?? "", sourceType: sourceType ?? "ios") + } + + internalLog(level: level, message: message, error: ddError, attributes: logAttributes) + } +} diff --git a/DatadogLogs/Sources/Scrubbing/LogEventMapper.swift b/DatadogLogs/Sources/Scrubbing/LogEventMapper.swift new file mode 100644 index 0000000000..beed4f9848 --- /dev/null +++ b/DatadogLogs/Sources/Scrubbing/LogEventMapper.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 + +/// Data scrubbing interface. +/// +/// It takes `LogEvent` and call the callback with the modified `LogEvent`. +/// Not calling the callback will drop the event. +public protocol LogEventMapper { + /// Maps a log event for data scrubbing. + /// + /// This method allow async call to the callback closure. + /// + /// - Parameters: + /// - event: The event to map. + /// - callback: The mapper callback with the new event. + func map(event: LogEvent, callback: @escaping (LogEvent) -> Void) +} + +/// Synchronous log event mapper. +/// +/// The class take a flat-map closure parameter for event scrubbing +internal final class SyncLogEventMapper: LogEventMapper { + let mapper: (LogEvent) -> LogEvent? + + init(_ mapper: @escaping (LogEvent) -> LogEvent?) { + self.mapper = mapper + } + + func map(event: LogEvent, callback: @escaping (LogEvent) -> Void) { + mapper(event).map(callback) + } +} diff --git a/DatadogLogs/Tests/ConsoleLoggerTests.swift b/DatadogLogs/Tests/ConsoleLoggerTests.swift new file mode 100644 index 0000000000..44565c4609 --- /dev/null +++ b/DatadogLogs/Tests/ConsoleLoggerTests.swift @@ -0,0 +1,131 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities + +@testable import DatadogLogs + +class ConsoleLoggerTests: XCTestCase { + private let mock = PrintFunctionMock() + + func testItPrintsMessageWithExpectedFormat() { + let randomPrefix: String = .mockRandom(length: 10) + + // Given + let logger = ConsoleLogger( + configuration: .init( + timeZone: .UTC, + format: .shortWith(prefix: randomPrefix) + ), + dateProvider: RelativeDateProvider( + using: .mockDecember15th2019At10AMUTC() + ), + printFunction: mock.print + ) + + // When + logger.debug("Debug message") + logger.info("Info message") + logger.notice("Notice message") + logger.warn("Warn message") + logger.error("Error message") + logger.critical("Critical message") + + // Then + XCTAssertEqual(mock.printedMessages.count, 6) + XCTAssertEqual(mock.printedMessages[0], "\(randomPrefix) 10:00:00.000 [DEBUG] Debug message") + XCTAssertEqual(mock.printedMessages[1], "\(randomPrefix) 10:00:00.000 [INFO] Info message") + XCTAssertEqual(mock.printedMessages[2], "\(randomPrefix) 10:00:00.000 [NOTICE] Notice message") + XCTAssertEqual(mock.printedMessages[3], "\(randomPrefix) 10:00:00.000 [WARN] Warn message") + XCTAssertEqual(mock.printedMessages[4], "\(randomPrefix) 10:00:00.000 [ERROR] Error message") + XCTAssertEqual(mock.printedMessages[5], "\(randomPrefix) 10:00:00.000 [CRITICAL] Critical message") + } + + func testItPrintsErrorWithExpectedFormat() { + // Given + let logger = ConsoleLogger( + configuration: .init( + timeZone: .UTC, + format: .short + ), + dateProvider: RelativeDateProvider( + using: .mockDecember15th2019At10AMUTC() + ), + printFunction: mock.print + ) + + let error = NSError( + domain: "The error domain", + code: 42, + userInfo: [NSLocalizedDescriptionKey: "A localized description of the error"] + ) + + // When + logger.debug("Message", error: error) + logger.info("Message", error: error) + logger.notice("Message", error: error) + logger.warn("Message", error: error) + logger.error("Message", error: error) + logger.critical("Message", error: error) + + // Then + let expectedMessages = ["[DEBUG]", "[INFO]", "[NOTICE]", "[WARN]", "[ERROR]", "[CRITICAL]"].map { status in + """ + 10:00:00.000 \(status) Message + + Error details: + → type: The error domain - 42 + → message: A localized description of the error + → stack: Error Domain=The error domain Code=42 "A localized description of the error" UserInfo={NSLocalizedDescription=A localized description of the error} + """ + } + zip(expectedMessages, mock.printedMessages).forEach { expected, actual in + XCTAssertEqual(expected, actual) + } + XCTAssertEqual(mock.printedMessages.count, 6) + } + + func testItPrintsErrorStringsWithExpectedFormat() { + // Given + let logger = ConsoleLogger( + configuration: .init( + timeZone: .UTC, + format: .short + ), + dateProvider: RelativeDateProvider( + using: .mockDecember15th2019At10AMUTC() + ), + printFunction: mock.print + ) + + let message = String.mockRandom() + let errorKind = String.mockRandom() + let errorMessage = String.mockRandom() + let stackTrace = String.mockRandom() + + logger._internal.log( + level: .info, + message: message, + errorKind: errorKind, + errorMessage: errorMessage, + stackTrace: stackTrace, + attributes: nil + ) + + // Then + let expectedMessage = """ + 10:00:00.000 [INFO] \(message) + + Error details: + → type: \(errorKind) + → message: \(errorMessage) + → stack: \(stackTrace) + """ + XCTAssertEqual(mock.printedMessages.first, expectedMessage) + XCTAssertEqual(mock.printedMessages.count, 1) + } +} diff --git a/DatadogLogs/Tests/Log/LogEventBuilderTests.swift b/DatadogLogs/Tests/Log/LogEventBuilderTests.swift new file mode 100644 index 0000000000..df8c6c1dd5 --- /dev/null +++ b/DatadogLogs/Tests/Log/LogEventBuilderTests.swift @@ -0,0 +1,321 @@ +/* + * 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 +@testable import DatadogLogs + +class LogEventBuilderTests: XCTestCase { + func testItBuildsLogEventWithLogInformation() throws { + let expectation = expectation(description: "build log event") + + let randomDate: Date = .mockRandomInThePast() + let randomLevel: LogLevel = .mockRandom() + let randomMessage: String = .mockRandom() + let randomError: DDError = .mockRandom() + let randomErrorFingerprint: String? = .mockRandom() + let randomAttributes: LogEvent.Attributes = .mockRandom() + let randomTags: Set = .mockRandom() + let randomService: String = .mockRandom() + let randomLoggerName: String = .mockRandom() + let randomThreadName: String = .mockRandom() + let randomOsName: String = .mockRandom() + let randomOsVersion: String = .mockRandom() + let randomOsBuildNumber: String = .mockRandom() + let randomName: String = .mockRandom() + let randomModel: String = .mockRandom() + let randomArchitecture: String = .mockRandom() + + // Given + let builder = LogEventBuilder( + service: randomService, + loggerName: randomLoggerName, + networkInfoEnabled: .mockAny(), + eventMapper: nil + ) + + // When + builder.createLogEvent( + date: randomDate, + level: randomLevel, + message: randomMessage, + error: randomError, + errorFingerprint: randomErrorFingerprint, + binaryImages: .mockAny(), + attributes: randomAttributes, + tags: randomTags, + context: .mockWith( + serverTimeOffset: 0, + device: .mockWith( + name: randomName, + model: randomModel, + osName: randomOsName, + osVersion: randomOsVersion, + osBuildNumber: randomOsBuildNumber, + architecture: randomArchitecture + ) + ), + threadName: randomThreadName + ) { log in + // Then + XCTAssertEqual(log.date, randomDate) + XCTAssertEqual(log.status, self.expectedLogStatus(for: randomLevel)) + XCTAssertEqual(log.message, randomMessage) + XCTAssertEqual(log.error?.kind, randomError.type) + XCTAssertEqual(log.error?.message, randomError.message) + XCTAssertEqual(log.error?.stack, randomError.stack) + XCTAssertEqual(log.error?.sourceType, "ios") + XCTAssertEqual(log.error?.fingerprint, randomErrorFingerprint) + XCTAssertEqual(log.attributes, randomAttributes) + XCTAssertEqual(log.tags.map { Set($0) }, randomTags) + XCTAssertEqual(log.serviceName, randomService) + XCTAssertEqual(log.loggerName, randomLoggerName) + XCTAssertEqual(log.threadName, randomThreadName) + XCTAssertEqual(log.dd.device.brand, "Apple") + XCTAssertEqual(log.dd.device.name, randomName) + XCTAssertEqual(log.dd.device.model, randomModel) + XCTAssertEqual(log.dd.device.architecture, randomArchitecture) + XCTAssertEqual(log.os.name, randomOsName) + XCTAssertEqual(log.os.version, randomOsVersion) + XCTAssertEqual(log.os.build, randomOsBuildNumber) + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0) + } + + func testItBuildsLogEventWithSDKContextInformation() throws { + let expectation = expectation(description: "build log event") + + let randomDate: Date = .mockRandomInThePast() + let randomApplicationVersion: String = .mockRandom() + let randomApplicationBuildNumber: String = .mockRandom() + let randomEnvironment: String = .mockRandom() + let randomSDKVersion: String = .mockRandom() + let randomUserInfo: UserInfo = .mockRandom() + let randomNetworkInfo: NetworkConnectionInfo = .mockRandom() + let randomCarrierInfo: CarrierInfo = .mockRandom() + let randomServerOffset: TimeInterval = .mockRandom(min: -10, max: 10) + let randomName: String = .mockRandom() + let randomModel: String = .mockRandom() + let randomOSVersion: String = .mockRandom() + let randomOSBuild: String = .mockRandom() + + let randomSDKContext: DatadogContext = .mockWith( + env: randomEnvironment, + version: randomApplicationVersion, + buildNumber: randomApplicationBuildNumber, + sdkVersion: randomSDKVersion, + serverTimeOffset: randomServerOffset, + device: .mockWith( + name: randomName, + model: randomModel, + osVersion: randomOSVersion, + osBuildNumber: randomOSBuild + ), + userInfo: randomUserInfo, + networkConnectionInfo: randomNetworkInfo, + carrierInfo: randomCarrierInfo + ) + + // Given + let builder = LogEventBuilder( + service: .mockAny(), + loggerName: .mockAny(), + networkInfoEnabled: true, + eventMapper: nil + ) + + // When + builder.createLogEvent( + date: randomDate, + level: .mockAny(), + message: .mockAny(), + error: .mockAny(), + errorFingerprint: .mockAny(), + binaryImages: .mockAny(), + attributes: .mockAny(), + tags: .mockAny(), + context: randomSDKContext, + threadName: .mockAny() + ) { log in + // Then + XCTAssertEqual(log.date, randomDate.addingTimeInterval(randomServerOffset), "It must correct date with server offset") + XCTAssertEqual(log.environment, randomSDKContext.env) + XCTAssertEqual(log.applicationVersion, randomSDKContext.version) + XCTAssertEqual(log.applicationBuildNumber, randomSDKContext.buildNumber) + XCTAssertEqual(log.loggerVersion, randomSDKContext.sdkVersion) + XCTAssertNil(log.buildId) + XCTAssertEqual(log.userInfo.id, randomUserInfo.id) + XCTAssertEqual(log.userInfo.name, randomUserInfo.name) + XCTAssertEqual(log.userInfo.email, randomUserInfo.email) + DDAssertDictionariesEqual(log.userInfo.extraInfo, randomUserInfo.extraInfo) + XCTAssertEqual(log.networkConnectionInfo, randomNetworkInfo) + XCTAssertEqual(log.mobileCarrierInfo, randomCarrierInfo) + XCTAssertEqual(log.dd.device.brand, "Apple") + XCTAssertEqual(log.dd.device.name, randomName) + XCTAssertEqual(log.dd.device.model, randomModel) + XCTAssertEqual(log.os.version, randomOSVersion) + XCTAssertEqual(log.os.build, randomOSBuild) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0) + } + + func testGivenContextWithBuildID_whenBuildingLog_itSetsBuildId() throws { + // Given + let buildId: String = .mockRandom() + let randomSDKContext: DatadogContext = .mockWith( + buildId: buildId + ) + + // When + let builder = LogEventBuilder( + service: .mockAny(), + loggerName: .mockAny(), + networkInfoEnabled: true, + eventMapper: nil + ) + + builder.createLogEvent( + date: .mockRandom(), + level: .mockAny(), + message: .mockAny(), + error: .mockAny(), + errorFingerprint: .mockAny(), + binaryImages: .mockAny(), + attributes: .mockAny(), + tags: .mockAny(), + context: randomSDKContext, + threadName: .mockAny() + ) { log in + XCTAssertEqual(log.buildId, buildId) + } + } + + func testGivenSendNetworkInfoDisabled_whenBuildingLog_itDoesNotSetConnectionAndCarrierInfo() throws { + let expectation = expectation(description: "build log event") + + // Given + let builder = LogEventBuilder( + service: .mockAny(), + loggerName: .mockAny(), + networkInfoEnabled: false, + eventMapper: nil + ) + + // When + builder.createLogEvent( + date: .mockAny(), + level: .mockAny(), + message: .mockAny(), + error: .mockAny(), + errorFingerprint: .mockAny(), + binaryImages: .mockAny(), + attributes: .mockAny(), + tags: .mockAny(), + context: .mockWith( + networkConnectionInfo: .mockAny(), + carrierInfo: .mockAny() + ), + threadName: .mockAny() + ) { log in + // Then + XCTAssertNil(log.networkConnectionInfo) + XCTAssertNil(log.mobileCarrierInfo) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0) + } + + // MARK: - Events Mapping + + func testGivenBuilderWithEventMapper_whenEventIsModified_itBuildsModifiedEvent() throws { + let expectation = expectation(description: "build log event") + + // Given + let builder = LogEventBuilder( + service: .mockAny(), + loggerName: .mockAny(), + networkInfoEnabled: .mockAny(), + eventMapper: SyncLogEventMapper { log in + var mutableLog = log + mutableLog.message = "modified message" + return mutableLog + } + ) + + // When + builder.createLogEvent( + date: .mockAny(), + level: .mockAny(), + message: "original message", + error: .mockAny(), + errorFingerprint: .mockAny(), + binaryImages: .mockAny(), + attributes: .mockAny(), + tags: .mockAny(), + context: .mockAny(), + threadName: .mockAny() + ) { log in + // Then + XCTAssertEqual(log.message, "modified message") + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0) + } + + func testGivenBuilderWithEventMapper_whenEventIsDropped_thenCallbackIsNotCalled() throws { + let expectation = expectation(description: "build log event") + expectation.isInverted = true + + // Given + let builder = LogEventBuilder( + service: .mockAny(), + loggerName: .mockAny(), + networkInfoEnabled: .mockAny(), + eventMapper: SyncLogEventMapper { _ in + return nil + } + ) + + // When + builder.createLogEvent( + date: .mockAny(), + level: .mockAny(), + message: .mockAny(), + error: .mockAny(), + errorFingerprint: .mockAny(), + binaryImages: .mockAny(), + attributes: .mockAny(), + tags: .mockAny(), + context: .mockAny(), + threadName: .mockAny() + ) { _ in + expectation.fulfill() + } + + wait(for: [expectation], timeout: 0) + } + + // MARK: - Helpers + + private func expectedLogStatus(for logLevel: LogLevel) -> LogEvent.Status { + switch logLevel { + case .debug: return .debug + case .info: return .info + case .notice: return .notice + case .warn: return .warn + case .error: return .error + case .critical: return .critical + } + } +} diff --git a/DatadogLogs/Tests/Log/LogSanitizerTests.swift b/DatadogLogs/Tests/Log/LogSanitizerTests.swift new file mode 100644 index 0000000000..16e5d28b46 --- /dev/null +++ b/DatadogLogs/Tests/Log/LogSanitizerTests.swift @@ -0,0 +1,259 @@ +/* + * 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 +@testable import DatadogLogs + +class LogSanitizerTests: XCTestCase { + /// Tracer Attributes shared with other Feature registered in core. + struct TracerAttributes { + static let traceID = "dd.trace_id" + static let spanID = "dd.span_id" + } + + /// RUM Attributes shared with other Feature registered in core. + enum RUMContextAttributes { + enum IDs { + /// The ID of RUM application (`String`). + static let applicationID = "application_id" + /// The ID of current RUM session (standard UUID `String`, lowercased). + /// In case the session is rejected (not sampled), RUM context is set to empty (`[:]`) in core. + static let sessionID = "session_id" + /// The ID of current RUM view (standard UUID `String`, lowercased). + static let viewID = "view.id" + /// The ID of current RUM action (standard UUID `String`, lowercased). + static let userActionID = "user_action.id" + } + } + + // MARK: - Attributes sanitization + + func testWhenUserAttributeUsesReservedName_itIsIgnored() { + let log = LogEvent.mockWith( + attributes: .mockWith( + userAttributes: [ + // reserved attributes: + "host": mockValue(), + "message": mockValue(), + "status": mockValue(), + "service": mockValue(), + "build_id": mockValue(), + "source": mockValue(), + "ddtags": mockValue(), + + // valid attributes: + "error.kind": mockValue(), + "error.message": mockValue(), + "error.stack": mockValue(), + "attribute1": mockValue(), + "attribute2": mockValue(), + "date": mockValue(), + ] + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 6) + XCTAssertNotNil(sanitized.attributes.userAttributes["attribute1"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["attribute2"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["date"]) + } + + func testWhenUserAttributeNameExceeds10NestedLevels_itIsEscapedByUnderscore() { + let log = LogEvent.mockWith( + attributes: .mockWith( + userAttributes: [ + "one": mockValue(), + "one.two": mockValue(), + "one.two.three": mockValue(), + "one.two.three.four": mockValue(), + "one.two.three.four.five": mockValue(), + "one.two.three.four.five.six": mockValue(), + "one.two.three.four.five.six.seven": mockValue(), + "one.two.three.four.five.six.seven.eight": mockValue(), + "one.two.three.four.five.six.seven.eight.nine": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten.eleven": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten.eleven.twelve": mockValue(), + ] + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 12) + XCTAssertNotNil(sanitized.attributes.userAttributes["one"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten_eleven"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten_eleven_twelve"]) + } + + func testWhenUserAttributeNameIsInvalid_itIsIgnored() { + let log = LogEvent.mockWith( + attributes: .mockWith( + userAttributes: [ + "valid-name": mockValue(), + "": mockValue(), // invalid name + ] + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 1) + XCTAssertNotNil(sanitized.attributes.userAttributes["valid-name"]) + } + + func testWhenNumberOfUserAttributesExceedsLimit_itDropsExtraOnes() { + let mockAttributes = (0...1_000).map { index in ("attribute-\(index)", mockValue()) } + let log = LogEvent.mockWith( + attributes: .mockWith( + userAttributes: Dictionary(uniqueKeysWithValues: mockAttributes) + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, AttributesSanitizer.Constraints.maxNumberOfAttributes) + } + + func testInternalAttributesAreNotSanitized() { + let log = LogEvent.mockWith( + attributes: .mockWith( + internalAttributes: [ + TracerAttributes.traceID: mockValue(), + TracerAttributes.spanID: mockValue(), + "attribute3": mockValue(), + ] + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.internalAttributes?.count, 3) + } + + func testReservedAttributesAreSanitized() { + let dd = DD.mockWith(logger: CoreLoggerMock()) + defer { dd.reset() } + + let log = LogEvent.mockWith( + attributes: .mockWith( + userAttributes: [ + TracerAttributes.traceID: mockValue(), + TracerAttributes.spanID: mockValue(), + RUMContextAttributes.IDs.applicationID: mockValue(), + RUMContextAttributes.IDs.sessionID: mockValue(), + RUMContextAttributes.IDs.viewID: mockValue(), + RUMContextAttributes.IDs.userActionID: mockValue(), + "attribute3": mockValue(), + ] + ) + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 1) + let logs = dd.logger.errorLogs + XCTAssertEqual(logs.count, 6) + dd.logger.errorLogs.forEach { + XCTAssertTrue($0.message.matches(regex: "'.*' is a reserved attribute name. This attribute will be ignored.")) + } + } + + // MARK: - Tags sanitization + + func testWhenTagHasUpperCasedCharacters_itGetsLowerCased() { + let log = LogEvent.mockWith( + tags: ["abcd", "Abcdef:ghi", "ABCDEF:GHIJK", "ABCDEFGHIJK"] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["abcd", "abcdef:ghi", "abcdef:ghijk", "abcdefghijk"]) + } + + func testWhenTagStartsWithIllegalCharacter_itIsIgnored() { + let log = LogEvent.mockWith( + tags: ["?invalid", "valid", "&invalid", ".abcdefghijk", ":abcd"] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["valid"]) + } + + func testWhenTagContainsIllegalCharacter_itIsConvertedToUnderscore() { + let log = LogEvent.mockWith( + tags: ["this&needs&underscore", "this*as*well", "this/doesnt", "tag with whitespaces"] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["this_needs_underscore", "this_as_well", "this/doesnt", "tag_with_whitespaces"]) + } + + func testWhenTagContainsTrailingCommas_itItTruncatesThem() { + let log = LogEvent.mockWith( + tags: ["with-one-comma:", "with-several-commas::::", "with-comma:in-the-middle"] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["with-one-comma", "with-several-commas", "with-comma:in-the-middle"]) + } + + func testWhenTagExceedsLengthLimit_itIsTruncated() { + let log = LogEvent.mockWith( + tags: [.mockRepeating(character: "a", times: 2 * LogEventSanitizer.Constraints.maxTagLength)] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual( + sanitized.tags, + [.mockRepeating(character: "a", times: LogEventSanitizer.Constraints.maxTagLength)] + ) + } + + func testWhenTagUsesReservedKey_itIsIgnored() { + let log = LogEvent.mockWith( + tags: ["host:abc", "device:abc", "source:abc", "service:abc", "valid"] + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["valid"]) + } + + func testWhenNumberOfTagsExceedsLimit_itDropsExtraOnes() { + let mockTags = (0...1_000).map { index in "tag\(index)" } + let log = LogEvent.mockWith( + tags: mockTags + ) + + let sanitized = LogEventSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags?.count, LogEventSanitizer.Constraints.maxNumberOfTags) + } + + // MARK: - Private + + private func mockValue() -> String { + return .mockAny() + } +} 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/LogMessageReceiverTests.swift b/DatadogLogs/Tests/LogMessageReceiverTests.swift new file mode 100644 index 0000000000..b2a599d904 --- /dev/null +++ b/DatadogLogs/Tests/LogMessageReceiverTests.swift @@ -0,0 +1,174 @@ +/* + * 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 +@testable import DatadogLogs + +class LogMessageReceiverTests: XCTestCase { + struct LogMessage: Encodable { + let logger: String + let service: String? + let date: Date + let message: String + let level: LogLevel + let thread: String + let error: DDError? + let networkInfoEnabled: Bool? + let userAttributes: [String: String]? + let internalAttributes: [String: String]? + } + + func testReceiveIncompleteLogMessage() throws { + let expectation = expectation(description: "Don't send log fallback") + + // Given + let core = PassthroughCoreMock( + context: .mockWith(service: "service-test"), + messageReceiver: LogMessageReceiver.mockAny() + ) + + // When + core.send( + message: .baggage( + key: "log", + value: "wrong-type" + ), + else: { expectation.fulfill() } + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertTrue(core.events.isEmpty) + } + + func testReceivePartialLogMessage() throws { + // Given + let core = PassthroughCoreMock( + context: .mockWith(service: "service-test"), + expectation: expectation(description: "Send log"), + messageReceiver: LogMessageReceiver.mockAny() + ) + + // When + core.send( + message: .baggage( + key: "log", + value: LogMessage( + logger: "logger-test", + service: nil, + date: .mockDecember15th2019At10AMUTC(), + message: "message-test", + level: .info, + thread: "thread-test", + error: nil, + networkInfoEnabled: nil, + userAttributes: nil, + internalAttributes: nil + ) + ) + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send log") + XCTAssertEqual(log.date, .mockDecember15th2019At10AMUTC()) + XCTAssertEqual(log.loggerName, "logger-test") + XCTAssertEqual(log.serviceName, "service-test") + XCTAssertEqual(log.threadName, "thread-test") + XCTAssertEqual(log.message, "message-test") + XCTAssertEqual(log.status, .info) + XCTAssertNil(log.error) + XCTAssertTrue(log.attributes.userAttributes.isEmpty) + XCTAssertNil(log.attributes.internalAttributes) + XCTAssertNil(log.networkConnectionInfo) + } + + func testReceiveCompleteLogMessage() throws { + // Given + let core = PassthroughCoreMock( + context: .mockAny(), + expectation: expectation(description: "Send log"), + messageReceiver: LogMessageReceiver.mockAny() + ) + + // When + core.send( + message: .baggage( + key: "log", + value: LogMessage( + logger: "logger-test", + service: "service-test", + date: .mockDecember15th2019At10AMUTC(), + message: "message-test", + level: .info, + thread: "thread-test", + error: .mockAny(), + networkInfoEnabled: true, + userAttributes: ["user": "attribute"], + internalAttributes: ["internal": "attribute"] + ) + ) + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + + let log: LogEvent = try XCTUnwrap(core.events().last, "It should send log") + XCTAssertEqual(log.date, .mockDecember15th2019At10AMUTC()) + XCTAssertEqual(log.loggerName, "logger-test") + XCTAssertEqual(log.serviceName, "service-test") + XCTAssertEqual(log.threadName, "thread-test") + XCTAssertEqual(log.message, "message-test") + XCTAssertEqual(log.status, .info) + XCTAssertEqual(log.error?.message, "abc") + DDAssertJSONEqual( + AnyEncodable(log.attributes.userAttributes), + ["user": "attribute"] + ) + DDAssertJSONEqual( + AnyEncodable(log.attributes.internalAttributes), + ["internal": "attribute"] + ) + XCTAssertNotNil(log.networkConnectionInfo) + } + + func testReceiveRejectedLogMessage() throws { + // Given + let core = PassthroughCoreMock( + context: .mockWith(service: "service-test"), + expectation: expectation(description: "Open scope but don't send log"), + messageReceiver: LogMessageReceiver( + logEventMapper: SyncLogEventMapper { _ in nil } + ) + ) + + // When + core.send( + message: .baggage( + key: "log", + value: LogMessage( + logger: "logger-test", + service: nil, + date: .mockDecember15th2019At10AMUTC(), + message: "message-test", + level: .info, + thread: "thread-test", + error: nil, + networkInfoEnabled: nil, + userAttributes: nil, + internalAttributes: nil + ) + ) + ) + + // Then + waitForExpectations(timeout: 0.5, handler: nil) + XCTAssertTrue(core.events.isEmpty) + } +} diff --git a/DatadogLogs/Tests/LoggerTests.swift b/DatadogLogs/Tests/LoggerTests.swift new file mode 100644 index 0000000000..6c61497eec --- /dev/null +++ b/DatadogLogs/Tests/LoggerTests.swift @@ -0,0 +1,163 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogLogs + +class LoggerTests: XCTestCase { + private var core: SingleFeatureCoreMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + core = SingleFeatureCoreMock(context: .mockWith(applicationBundleIdentifier: "com.datadog.unit-tests")) + Logs.enable(in: core) + } + + override func tearDown() { + core = nil + super.tearDown() + } + + func testDefaultLogger() throws { + let logger = Logger.create(in: core) + + let remoteLogger = try XCTUnwrap(logger as? RemoteLogger) + XCTAssertNil(remoteLogger.configuration.service) + XCTAssertNil(remoteLogger.configuration.name) + XCTAssertFalse(remoteLogger.configuration.networkInfoEnabled) + XCTAssertEqual(remoteLogger.configuration.threshold, .debug) + XCTAssertEqual(remoteLogger.configuration.sampler.samplingRate, 100) + XCTAssertNil(remoteLogger.configuration.eventMapper) + XCTAssertTrue(remoteLogger.rumContextIntegration) + XCTAssertTrue(remoteLogger.activeSpanIntegration) + } + + func testDefaultLoggerWithRUMEnabled() throws { + let logger1 = Logger.create(in: core) + XCTAssertTrue(try XCTUnwrap(logger1 as? RemoteLogger).rumContextIntegration) + + let logger2 = Logger.create( + with: Logger.Configuration( + bundleWithRumEnabled: false + ), + in: core + ) + XCTAssertFalse(try XCTUnwrap(logger2 as? RemoteLogger).rumContextIntegration) + } + + func testDefaultLoggerWithTracingEnabled() throws { + let logger1 = Logger.create(in: core) + XCTAssertTrue(try XCTUnwrap(logger1 as? RemoteLogger).activeSpanIntegration) + + let logger2 = Logger.create( + with: Logger.Configuration( + bundleWithTraceEnabled: false + ), + in: core + ) + XCTAssertFalse(try XCTUnwrap(logger2 as? RemoteLogger).activeSpanIntegration) + } + + func testCustomizedLogger() throws { + let logger = Logger.create( + with: Logger.Configuration( + service: "custom-service-name", + name: "custom-logger-name", + networkInfoEnabled: true, + bundleWithRumEnabled: false, + bundleWithTraceEnabled: false, + remoteSampleRate: 50, + remoteLogThreshold: .error + ), + in: core + ) + + let remoteLogger = try XCTUnwrap(logger as? RemoteLogger) + XCTAssertEqual(remoteLogger.configuration.service, "custom-service-name") + XCTAssertEqual(remoteLogger.configuration.name, "custom-logger-name") + XCTAssertTrue(remoteLogger.configuration.networkInfoEnabled) + XCTAssertEqual(remoteLogger.configuration.threshold, .error) + XCTAssertNil(remoteLogger.configuration.eventMapper) + XCTAssertFalse(remoteLogger.rumContextIntegration) + XCTAssertFalse(remoteLogger.activeSpanIntegration) + XCTAssertEqual(remoteLogger.configuration.sampler.samplingRate, 50) + } + + func testCombiningInternalLoggers() throws { + var logger: LoggerProtocol + + logger = Logger.create(in: core) + XCTAssertTrue(logger is RemoteLogger) + + logger = Logger.create(with: Logger.Configuration(remoteSampleRate: .random(in: 1...100)), in: core) + XCTAssertTrue(logger is RemoteLogger) + + logger = Logger.create(with: Logger.Configuration(remoteSampleRate: 0), in: core) + XCTAssertTrue(logger is NOPLogger) + + logger = Logger.create(with: Logger.Configuration(consoleLogFormat: .short), in: core) + var combinedLogger = try XCTUnwrap(logger as? CombinedLogger) + XCTAssertTrue(combinedLogger.combinedLoggers[0] is RemoteLogger) + XCTAssertTrue(combinedLogger.combinedLoggers[1] is ConsoleLogger) + + logger = Logger.create(with: Logger.Configuration(consoleLogFormat: nil), in: core) + XCTAssertTrue(logger is RemoteLogger) + + logger = Logger.create( + with: Logger.Configuration( + remoteSampleRate: 100, + consoleLogFormat: .short + ), + in: core + ) + combinedLogger = try XCTUnwrap(logger as? CombinedLogger) + XCTAssertTrue(combinedLogger.combinedLoggers[0] is RemoteLogger) + XCTAssertTrue(combinedLogger.combinedLoggers[1] is ConsoleLogger) + + logger = Logger.create( + with: Logger.Configuration( + remoteSampleRate: 0, + consoleLogFormat: .short + ), + in: core + ) + XCTAssertTrue(logger is ConsoleLogger) + + logger = Logger.create( + with: Logger.Configuration( + remoteSampleRate: 100, + consoleLogFormat: nil + ), + in: core + ) + XCTAssertTrue(logger is RemoteLogger) + + logger = Logger.create( + with: Logger.Configuration( + remoteSampleRate: 0, + consoleLogFormat: nil + ), + in: core + ) + XCTAssertTrue(logger is NOPLogger) + } + + func testConfiguration_withDebug_itDisableSampling() throws { + //Given + var config = Logger.Configuration(remoteSampleRate: 50) + config.processInfo = ProcessInfoMock(arguments: [LaunchArguments.Debug]) + + // When + let logger = Logger.create(with: config, in: core) + + // Then + let remoteLogger = try XCTUnwrap(logger as? RemoteLogger) + XCTAssertEqual(remoteLogger.configuration.sampler.samplingRate, 100) + } +} diff --git a/DatadogLogs/Tests/LogsTests.swift b/DatadogLogs/Tests/LogsTests.swift new file mode 100644 index 0000000000..ba16749304 --- /dev/null +++ b/DatadogLogs/Tests/LogsTests.swift @@ -0,0 +1,173 @@ +/* + * 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 +import TestUtilities + +@testable import DatadogLogs + +class LogsTests: XCTestCase { + func testDefaultConfiguration() { + // Given + let config = Logs.Configuration() + + // Then + XCTAssertNil(config.eventMapper) + XCTAssertNil(config.customEndpoint) + } + + func testWhenNotEnabled_thenLogsIsEnabledIsFalse() { + // When + let core = FeatureRegistrationCoreMock() + XCTAssertNil(core.get(feature: LogsFeature.self)) + + // Then + XCTAssertFalse(Logs._internal.isEnabled(in: core)) + } + + func testWhenEnabled_thenLogsIsEnabledIsTrue() { + // When + let core = FeatureRegistrationCoreMock() + let config = Logs.Configuration() + Logs.enable(with: config, in: core) + + // Then + 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() + + let core = SingleFeatureCoreMock() + + // When + Logs.enable( + with: Logs.Configuration( + eventMapper: { $0 }, + customEndpoint: customEndpoint + ), + in: core + ) + + // Then + let logs = try XCTUnwrap(core.get(feature: LogsFeature.self)) + let requestBuilder = try XCTUnwrap(logs.requestBuilder as? RequestBuilder) + XCTAssertNotNil(logs.logEventMapper) + XCTAssertEqual(requestBuilder.customIntakeURL, customEndpoint) + } + + func testConfigurationInternalOverrides() throws { + struct LogEventMapperMock: LogEventMapper { + func map(event: DatadogLogs.LogEvent, callback: @escaping (DatadogLogs.LogEvent) -> Void) { + callback(event) + } + } + + // Given + let eventMapper = LogEventMapperMock() + var config = Logs.Configuration() + + // When + config._internal_mutation { + $0.setLogEventMapper(eventMapper) + } + + // Then + XCTAssertTrue(config._internalEventMapper is LogEventMapperMock) + } + + func testLogsAddAttributeForwardedToFeature() throws { + // Given + let core = FeatureRegistrationCoreMock() + let config = Logs.Configuration() + Logs.enable(with: config, in: core) + + // When + let attributeKey: String = .mockRandom() + let attributeValue: String = .mockRandom() + Logs.addAttribute(forKey: attributeKey, value: attributeValue, in: core) + + // Then + let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) + XCTAssertEqual(feature.attributes.getAttributes()[attributeKey] as? String, attributeValue) + } + + func testLogsRemoveAttributeForwardedToFeature() throws { + // Given + let core = FeatureRegistrationCoreMock() + let config = Logs.Configuration() + Logs.enable(with: config, in: core) + let attributeKey: String = .mockRandom() + let attributeValue: String = .mockRandom() + Logs.addAttribute(forKey: attributeKey, value: attributeValue, in: core) + + // When + Logs.removeAttribute(forKey: attributeKey, in: core) + + // Then + let feature = try XCTUnwrap(core.get(feature: LogsFeature.self)) + XCTAssertNil(feature.attributes.getAttributes()[attributeKey]) + } + + func testItSendsGlobalLogUpdates_whenAddAttribute() throws { + // Given + let mockMessageReciever = FeatureMessageReceiverMock() + let core = SingleFeatureCoreMock( + messageReceiver: mockMessageReciever + ) + let config = Logs.Configuration() + Logs.enable(with: config, in: core) + + // When + let attributeKey: String = .mockRandom() + let attributeValue: String = .mockRandom() + Logs.addAttribute(forKey: attributeKey, value: attributeValue, in: core) + + // Then + let logMessage: [FeatureMessage] = mockMessageReciever.messages.filter { $0.asBaggage?.key == GlobalLogAttributes.key } + XCTAssertEqual(logMessage.count, 1) + let message = try XCTUnwrap(logMessage.first) + let baggage: GlobalLogAttributes = try XCTUnwrap(message.baggage(forKey: GlobalLogAttributes.key)) + XCTAssertEqual((baggage.attributes[attributeKey] as? AnyCodable)?.value as? String, attributeValue) + } + + func testItSendsGlobalLogUpdates_whenRemoveAttribute() throws { + // Given + let mockMessageReciever = FeatureMessageReceiverMock() + let core = SingleFeatureCoreMock( + messageReceiver: mockMessageReciever + ) + let config = Logs.Configuration() + Logs.enable(with: config, in: core) + let attributeKey: String = .mockRandom() + let attributeValue: String = .mockRandom() + Logs.addAttribute(forKey: attributeKey, value: attributeValue, in: core) + + // When + Logs.removeAttribute(forKey: attributeKey, in: core) + + // Then + let logMessage: [FeatureMessage] = mockMessageReciever.messages.filter { $0.asBaggage?.key == GlobalLogAttributes.key } + XCTAssertEqual(logMessage.count, 2) + let message = try XCTUnwrap(logMessage.last) + let baggage: GlobalLogAttributes = try XCTUnwrap(message.baggage(forKey: GlobalLogAttributes.key)) + XCTAssertNil(baggage.attributes[attributeKey]) + } +} diff --git a/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift new file mode 100644 index 0000000000..275a5837ee --- /dev/null +++ b/DatadogLogs/Tests/Mocks/LoggingFeatureMocks.swift @@ -0,0 +1,340 @@ +/* + * 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 TestUtilities +import DatadogInternal + +@testable import DatadogLogs + +extension RemoteLogger.Configuration: AnyMockable { + public static func mockAny() -> Self { .mockWith() } + + static func mockWith( + service: String? = "logger.tests", + name: String? = "TestLogger", + networkInfoEnabled: Bool = false, + threshold: LogLevel = .info, + eventMapper: LogEventMapper? = nil, + sampler: Sampler = .mockKeepAll() + ) -> Self { + return .init( + service: service, + name: name, + networkInfoEnabled: networkInfoEnabled, + threshold: threshold, + eventMapper: eventMapper, + sampler: sampler + ) + } +} + +extension LogsFeature { + /// Mocks an instance of the feature that performs no writes to file system and does no uploads. + static func mockAny() -> Self { .mockWith() } + + /// Mocks an instance of the feature that performs no writes to file system and does no uploads. + static func mockWith( + logEventMapper: LogEventMapper? = nil, + sampler: Sampler = .mockKeepAll(), + requestBuilder: FeatureRequestBuilder = RequestBuilder(), + messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), + dateProvider: DateProvider = SystemDateProvider(), + backtraceReporter: BacktraceReporting = BacktraceReporterMock(backtrace: nil) + ) -> Self { + return .init( + logEventMapper: logEventMapper, + requestBuilder: requestBuilder, + messageReceiver: messageReceiver, + dateProvider: dateProvider, + backtraceReporter: backtraceReporter + ) + } +} + +extension LogMessageReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + public static func mockWith( + logEventMapper: LogEventMapper? = nil + ) -> Self { + .init( + logEventMapper: logEventMapper + ) + } +} + +extension CrashLogReceiver: AnyMockable { + public static func mockAny() -> Self { + .mockWith() + } + + public static func mockWith( + dateProvider: DateProvider = SystemDateProvider() + ) -> Self { + .init( + dateProvider: dateProvider, + logEventMapper: nil + ) + } +} + +// MARK: - Log Mocks + +extension LogLevel: AnyMockable, RandomMockable { + public static func mockAny() -> LogLevel { + return .debug + } + + public static func mockRandom() -> LogLevel { + return [ + LogLevel.debug, + LogLevel.info, + LogLevel.notice, + LogLevel.warn, + LogLevel.error, + LogLevel.critical, + ].randomElement()! + } +} + +extension LogEvent: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent { + return mockWith() + } + + public static func mockWith( + date: Date = .mockAny(), + status: LogEvent.Status = .mockAny(), + message: String = .mockAny(), + error: LogEvent.Error? = nil, + serviceName: String = .mockAny(), + environment: String = .mockAny(), + loggerName: String = .mockAny(), + loggerVersion: String = .mockAny(), + threadName: String = .mockAny(), + applicationVersion: String = .mockAny(), + applicationBuildNumber: String = .mockAny(), + buildId: String? = .mockAny(), + variant: String? = .mockAny(), + dd: LogEvent.Dd = .mockAny(), + os: LogEvent.OperatingSystem = .mockAny(), + userInfo: UserInfo = .mockAny(), + networkConnectionInfo: NetworkConnectionInfo = .mockAny(), + mobileCarrierInfo: CarrierInfo? = .mockAny(), + attributes: LogEvent.Attributes = .mockAny(), + tags: [String]? = nil + ) -> LogEvent { + return LogEvent( + date: date, + status: status, + message: message, + error: error, + serviceName: serviceName, + environment: environment, + loggerName: loggerName, + loggerVersion: loggerVersion, + threadName: threadName, + applicationVersion: applicationVersion, + applicationBuildNumber: applicationBuildNumber, + buildId: nil, + variant: variant, + dd: dd, + os: os, + userInfo: userInfo, + networkConnectionInfo: networkConnectionInfo, + mobileCarrierInfo: mobileCarrierInfo, + attributes: attributes, + tags: tags + ) + } + + public static func mockRandom() -> LogEvent { + return LogEvent( + date: .mockRandomInThePast(), + status: .mockRandom(), + message: .mockRandom(), + error: .mockRandom(), + serviceName: .mockRandom(), + environment: .mockRandom(), + loggerName: .mockRandom(), + loggerVersion: .mockRandom(), + threadName: .mockRandom(), + applicationVersion: .mockRandom(), + applicationBuildNumber: .mockRandom(), + buildId: .mockRandom(), + variant: .mockRandom(), + dd: .mockRandom(), + os: .mockRandom(), + userInfo: .mockRandom(), + networkConnectionInfo: .mockRandom(), + mobileCarrierInfo: .mockRandom(), + attributes: .mockRandom(), + tags: .mockRandom() + ) + } +} + +extension LogEvent.Status: RandomMockable { + public static func mockAny() -> LogEvent.Status { + return .info + } + + public static func mockRandom() -> LogEvent.Status { + return allCases.randomElement()! + } +} + +extension LogEvent.UserInfo: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.UserInfo { + return mockEmpty() + } + + public static func mockEmpty() -> LogEvent.UserInfo { + return LogEvent.UserInfo( + id: nil, + name: nil, + email: nil, + extraInfo: [:] + ) + } + + public static func mockRandom() -> LogEvent.UserInfo { + return .init( + id: .mockRandom(), + name: .mockRandom(), + email: .mockRandom(), + extraInfo: mockRandomAttributes() + ) + } +} + +extension LogEvent.Dd: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.Dd { + return LogEvent.Dd( + device: .mockAny() + ) + } + + public static func mockRandom() -> LogEvent.Dd { + return LogEvent.Dd( + device: .mockRandom() + ) + } +} + +extension LogEvent.DeviceInfo: AnyMockable, RandomMockable { + public static func mockAny() -> LogEvent.DeviceInfo { + return LogEvent.DeviceInfo( + brand: .mockAny(), + name: .mockAny(), + model: .mockAny(), + architecture: .mockAny() + ) + } + + public static func mockRandom() -> LogEvent.DeviceInfo { + return LogEvent.DeviceInfo( + brand: .mockRandom(), + name: .mockRandom(), + model: .mockRandom(), + architecture: .mockRandom() + ) + } +} + +extension LogEvent.OperatingSystem: AnyMockable, RandomMockable { + public static func mockAny() -> Self { + .init( + name: .mockAny(), + version: .mockAny(), + build: .mockAny() + ) + } + + public static func mockRandom() -> Self { + .init( + name: .mockRandom(), + version: .mockRandom(), + build: .mockRandom() + ) + } +} + +extension LogEvent.Error: RandomMockable { + public static func mockRandom() -> Self { + return .init( + kind: .mockRandom(), + message: .mockRandom(), + stack: .mockRandom() + ) + } +} + +// MARK: - Component Mocks + +extension LogEventBuilder: AnyMockable { + public static func mockAny() -> LogEventBuilder { + return mockWith() + } + + public static func mockWith( + service: String = .mockAny(), + loggerName: String = .mockAny(), + networkInfoEnabled: Bool = .mockAny(), + eventMapper: LogEventMapper? = nil, + deviceInfo: DeviceInfo = .mockAny() + ) -> LogEventBuilder { + return LogEventBuilder( + service: service, + loggerName: loggerName, + networkInfoEnabled: networkInfoEnabled, + eventMapper: eventMapper + ) + } +} + +extension LogEvent.Attributes: Equatable { + public static func mockAny() -> LogEvent.Attributes { + return mockWith() + } + + public static func mockWith( + userAttributes: [String: Encodable] = [:], + internalAttributes: [String: Encodable]? = [:] + ) -> LogEvent.Attributes { + return LogEvent.Attributes( + userAttributes: userAttributes, + internalAttributes: internalAttributes + ) + } + + public static func mockRandom() -> LogEvent.Attributes { + return .init( + userAttributes: mockRandomAttributes(), + internalAttributes: mockRandomAttributes() + ) + } + + public static func == (lhs: LogEvent.Attributes, rhs: LogEvent.Attributes) -> Bool { + let lhsUserAttributesSorted = lhs.userAttributes.sorted { $0.key < $1.key } + let rhsUserAttributesSorted = rhs.userAttributes.sorted { $0.key < $1.key } + + let lhsInternalAttributesSorted = lhs.internalAttributes?.sorted { $0.key < $1.key } + let rhsInternalAttributesSorted = rhs.internalAttributes?.sorted { $0.key < $1.key } + + return String(describing: lhsUserAttributesSorted) == String(describing: rhsUserAttributesSorted) + && 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 new file mode 100644 index 0000000000..6b98bfd8c3 --- /dev/null +++ b/DatadogLogs/Tests/RemoteLoggerTests.swift @@ -0,0 +1,626 @@ +/* + * 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 +@testable import DatadogLogs + +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 + let type: String? + /// The Log error stack + let stack: String? + /// The Log error stack + let source: String + /// The Log attributes + let attributes: [String: AnyCodable] + /// Binary images + let binaryImages: [BinaryImage]? + } + + func testWhenNonErrorLogged_itDoesNotPostsToMessageBus() throws { + // Given + let logger = RemoteLogger( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() + ) + + // When + logger.info("Info message") + + // Then + XCTAssertEqual(featureScope.messagesSent().count, 0) + } + + 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( + featureScope: featureScope, + globalAttributes: .mockAny(), + configuration: .mockAny(), + dateProvider: RelativeDateProvider(), + rumContextIntegration: false, + activeSpanIntegration: false, + backtraceReporter: BacktraceReporterMock() + ) + + // When + logger.error("Error message", error: nil, attributes: [CrossPlatformAttributes.errorLogIsCrash: true]) + + // Then + 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) + ) + + // 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.. "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + s.swift_version = '5.9' + s.ios.deployment_target = '12.0' + s.tvos.deployment_target = '12.0' + + s.source = { :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => s.version.to_s } + + s.source_files = "DatadogObjc/Sources/**/*.swift" + s.dependency 'DatadogCore', s.version.to_s + s.dependency 'DatadogRUM', s.version.to_s + s.dependency 'DatadogLogs', s.version.to_s + s.dependency 'DatadogTrace', s.version.to_s +end diff --git a/DatadogObjc/Sources/DDInternalLogger+objc.swift b/DatadogObjc/Sources/DDInternalLogger+objc.swift new file mode 100644 index 0000000000..17125114c5 --- /dev/null +++ b/DatadogObjc/Sources/DDInternalLogger+objc.swift @@ -0,0 +1,42 @@ +/* + * 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 + +@objc +public class DDInternalLogger: NSObject { + /// Function printing `String` content to console. Intended to be used only by SDK components. + @objc + public static func consolePrint(_ message: String, _ level: DDCoreLoggerLevel) { + let coreLoggerLevel: CoreLoggerLevel = switch level { + case .debug: .debug + case .warn: .warn + case .error: .error + case .critical: .critical + } + DatadogInternal.consolePrint(message, coreLoggerLevel) + } + + @objc + public static func telemetryDebug(id: String, message: String) { + Datadog._internal.telemetry.debug(id: id, message: message) + } + + @objc + public static func telemetryError(id: String, message: String, kind: String?, stack: String?) { + Datadog._internal.telemetry.error(id: id, message: message, kind: kind, stack: stack) + } +} + +@objc +public enum DDCoreLoggerLevel: Int { + case debug + case warn + case error + case critical +} diff --git a/DatadogObjc/Sources/DDURLSessionDelegate+objc.swift b/DatadogObjc/Sources/DDURLSessionDelegate+objc.swift new file mode 100644 index 0000000000..cec28dcfbb --- /dev/null +++ b/DatadogObjc/Sources/DDURLSessionDelegate+objc.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 DatadogCore +import DatadogInternal + +@objc +@available(*, deprecated, message: "Use `URLSessionInstrumentation.enable(with:)` instead.") +open class DDNSURLSessionDelegate: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate { + @objc + override public init() { + URLSessionInstrumentation.enable( + with: .init( + delegateClass: Self.self + ), + in: CoreRegistry.default + ) + super.init() + } + + @objc + public init(additionalFirstPartyHostsWithHeaderTypes: [String: Set]) { + URLSessionInstrumentation.enable( + with: .init( + delegateClass: Self.self, + firstPartyHostsTracing: .traceWithHeaders(hostsWithHeaders: additionalFirstPartyHostsWithHeaderTypes.mapValues { tracingHeaderTypes in + return Set(tracingHeaderTypes.map { $0.swiftType }) + }) + ), + in: CoreRegistry.default + ) + super.init() + } + + @objc + public convenience init(additionalFirstPartyHosts: Set) { + self.init( + additionalFirstPartyHostsWithHeaderTypes: additionalFirstPartyHosts.reduce(into: [:], { partialResult, host in + partialResult[host] = [.datadog, .tracecontext] + }) + ) + } +} diff --git a/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift b/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift new file mode 100644 index 0000000000..6c8195c76a --- /dev/null +++ b/DatadogObjc/Sources/DDURLSessionInstrumentation+objc.swift @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogCore +import DatadogInternal + +/// Configuration of URLSession instrumentation. +@objc +public class DDURLSessionInstrumentationConfiguration: NSObject { + internal var swiftConfig: URLSessionInstrumentation.Configuration + + @objc + public init(delegateClass: URLSessionDataDelegate.Type) { + swiftConfig = .init(delegateClass: delegateClass) + } + + /// Sets additional first party hosts to consider in the interception. + @objc + public func setFirstPartyHostsTracing(_ firstPartyHostsTracing: DDURLSessionInstrumentationFirstPartyHostsTracing) { + swiftConfig.firstPartyHostsTracing = firstPartyHostsTracing.swiftType + } + + /// The delegate class to be used to swizzle URLSessionTaskDelegate & URLSessionDataDelegate methods. + @objc public var delegateClass: URLSessionDataDelegate.Type { + set { swiftConfig.delegateClass = newValue } + get { swiftConfig.delegateClass } + } +} + +/// Defines configuration for first-party hosts in distributed tracing. +@objc +public class DDURLSessionInstrumentationFirstPartyHostsTracing: NSObject { + internal var swiftType: URLSessionInstrumentation.FirstPartyHostsTracing + + @objc + public init(hostsWithHeaderTypes: [String: Set]) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in + Set(headerTypes.map { + $0.swiftType + }) + } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders) + } + + @objc + public init(hosts: Set) { + swiftType = .trace(hosts: hosts) + } +} + +@objc +public class DDURLSessionInstrumentation: NSObject { + /// Enables URLSession instrumentation. + /// + /// - Parameters: + /// - configuration: Configuration of the feature. + @objc + public static func enable(configuration: DDURLSessionInstrumentationConfiguration) { + URLSessionInstrumentation.enable(with: configuration.swiftConfig) + } + + /// Disables URLSession instrumentation. + /// - Parameters: + /// - delegateClass: The delegate class to unbind. + @objc + public static func disable(delegateClass: URLSessionDataDelegate.Type) { + URLSessionInstrumentation.disable(delegateClass: delegateClass) + } +} diff --git a/DatadogObjc/Sources/Datadog+objc.swift b/DatadogObjc/Sources/Datadog+objc.swift new file mode 100644 index 0000000000..2d626e75fb --- /dev/null +++ b/DatadogObjc/Sources/Datadog+objc.swift @@ -0,0 +1,104 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogCore +import DatadogInternal + +@objc +public class DDTrackingConsent: NSObject { + internal let sdkConsent: TrackingConsent + + internal init(sdkConsent: TrackingConsent) { + self.sdkConsent = sdkConsent + } + + // MARK: - Public + + @objc + public static func granted() -> DDTrackingConsent { .init(sdkConsent: .granted) } + + @objc + public static func notGranted() -> DDTrackingConsent { .init(sdkConsent: .notGranted) } + + @objc + public static func pending() -> DDTrackingConsent { .init(sdkConsent: .pending) } +} + +@objc +public class DDDatadog: NSObject { + // MARK: - Public + + @objc + public static func initialize( + configuration: DDConfiguration, + trackingConsent: DDTrackingConsent + ) { + Datadog.initialize( + with: configuration.sdkConfiguration, + trackingConsent: trackingConsent.sdkConsent + ) + } + + @objc + public static func setVerbosityLevel(_ verbosityLevel: DDSDKVerbosityLevel) { + switch verbosityLevel { + case .debug: Datadog.verbosityLevel = .debug + case .warn: Datadog.verbosityLevel = .warn + case .error: Datadog.verbosityLevel = .error + case .critical: Datadog.verbosityLevel = .critical + case .none: Datadog.verbosityLevel = nil + } + } + + @objc + public static func verbosityLevel() -> DDSDKVerbosityLevel { + switch Datadog.verbosityLevel { + case .debug: return .debug + case .warn: return .warn + case .error: return .error + case .critical: return .critical + case .none: return .none + } + } + + @objc + public static func setUserInfo(id: String? = nil, name: String? = nil, email: String? = nil, extraInfo: [String: Any] = [:]) { + Datadog.setUserInfo(id: id, name: name, email: email, extraInfo: extraInfo.dd.swiftAttributes) + } + + @objc + public static func addUserExtraInfo(_ extraInfo: [String: Any]) { + Datadog.addUserExtraInfo(extraInfo.dd.swiftAttributes) + } + + @objc + public static func setTrackingConsent(consent: DDTrackingConsent) { + Datadog.set(trackingConsent: consent.sdkConsent) + } + + @objc + public static func isInitialized() -> Bool { + return Datadog.isInitialized() + } + + @objc + public static func stopInstance() { + Datadog.stopInstance() + } + + @objc + public static func clearAllData() { + Datadog.clearAllData() + } + +#if DD_SDK_COMPILED_FOR_TESTING + @objc + public static func flushAndDeinitialize() { + Datadog.flushAndDeinitialize() + } +#endif +} diff --git a/DatadogObjc/Sources/DatadogConfiguration+objc.swift b/DatadogObjc/Sources/DatadogConfiguration+objc.swift new file mode 100644 index 0000000000..0eb13fc45d --- /dev/null +++ b/DatadogObjc/Sources/DatadogConfiguration+objc.swift @@ -0,0 +1,289 @@ +/* + * 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 + +@objc +public class DDSite: NSObject { + internal let sdkSite: DatadogSite + + internal init(sdkSite: DatadogSite) { + self.sdkSite = sdkSite + } + + // MARK: - Public + + @objc + public static func us1() -> DDSite { .init(sdkSite: .us1) } + + @objc + public static func us3() -> DDSite { .init(sdkSite: .us3) } + + @objc + public static func us5() -> DDSite { .init(sdkSite: .us5) } + + @objc + public static func eu1() -> DDSite { .init(sdkSite: .eu1) } + + @objc + public static func ap1() -> DDSite { .init(sdkSite: .ap1) } + + @objc + public static func us1_fed() -> DDSite { .init(sdkSite: .us1_fed) } +} + +@objc +public enum DDBatchSize: Int { + case small + case medium + case large + + internal var swiftType: Datadog.Configuration.BatchSize { + switch self { + case .small: return .small + case .medium: return .medium + case .large: return .large + } + } + + internal init(swiftType: Datadog.Configuration.BatchSize) { + switch swiftType { + case .small: self = .small + case .medium: self = .medium + case .large: self = .large + } + } +} + +@objc +public enum DDUploadFrequency: Int { + case frequent + case average + case rare + + internal var swiftType: Datadog.Configuration.UploadFrequency { + switch self { + case .frequent: return .frequent + case .average: return .average + case .rare: return .rare + } + } + + internal init(swiftType: Datadog.Configuration.UploadFrequency) { + switch swiftType { + case .frequent: self = .frequent + case .average: self = .average + case .rare: self = .rare + } + } +} + +@objc +public enum DDBatchProcessingLevel: Int { + case low + case medium + case high + + internal var swiftType: Datadog.Configuration.BatchProcessingLevel { + switch self { + case .low: return .low + case .medium: return .medium + case .high: return .high + } + } + + internal init(swiftType: Datadog.Configuration.BatchProcessingLevel) { + switch swiftType { + case .low: self = .low + case .medium: self = .medium + case .high: self = .high + } + } +} + +@objc +public class DDTracingHeaderType: NSObject { + internal let swiftType: TracingHeaderType + + private init(_ swiftType: TracingHeaderType) { + self.swiftType = swiftType + } + + @objc public static let datadog = DDTracingHeaderType(.datadog) + @objc public static let b3multi = DDTracingHeaderType(.b3multi) + @objc public static let b3 = DDTracingHeaderType(.b3) + @objc public static let tracecontext = DDTracingHeaderType(.tracecontext) +} + +@objc +public protocol DDDataEncryption: AnyObject { + /// Encrypts given `Data` with user-chosen encryption. + /// + /// - Parameter data: Data to encrypt. + /// - Returns: The encrypted data. + func encrypt(data: Data) throws -> Data + + /// Decrypts given `Data` with user-chosen encryption. + /// + /// Beware that data to decrypt could be encrypted in a previous + /// app launch, so implementation should be aware of the case when decryption could + /// fail (for example, key used for encryption is different from key used for decryption, if + /// they are unique for every app launch). + /// + /// - Parameter data: Data to decrypt. + /// - Returns: The decrypted data. + func decrypt(data: Data) throws -> Data +} + +internal struct DDDataEncryptionBridge: DataEncryption { + let objcEncryption: DDDataEncryption + + func encrypt(data: Data) throws -> Data { + return try objcEncryption.encrypt(data: data) + } + + func decrypt(data: Data) throws -> Data { + return try objcEncryption.decrypt(data: data) + } +} + +@objc +public protocol DDServerDateProvider: AnyObject { + /// Start the clock synchronisation with NTP server. + /// + /// Calls the `completion` by passing it the server time offset when the synchronization succeeds or`nil` if it fails. + func synchronize(update: @escaping (TimeInterval) -> Void) +} + +internal struct DDServerDateProviderBridge: ServerDateProvider { + let objcProvider: DDServerDateProvider + + func synchronize(update: @escaping (TimeInterval) -> Void) { + objcProvider.synchronize(update: update) + } +} + +@objc +public class DDConfiguration: NSObject { + internal var sdkConfiguration: Datadog.Configuration + + /// Either the RUM client token (which supports RUM, Logging and APM) or regular client token, only for Logging and APM. + @objc public var clientToken: String { + get { sdkConfiguration.clientToken } + set { sdkConfiguration.clientToken = newValue } + } + + /// The environment name which will be sent to Datadog. This can be used + /// To filter events on different environments (e.g. "staging" or "production"). + @objc public var env: String { + get { sdkConfiguration.env } + set { sdkConfiguration.env = newValue } + } + + /// The Datadog server site where data is sent. + /// + /// Default value is `.us1`. + @objc public var site: DDSite { + get { DDSite(sdkSite: sdkConfiguration.site) } + set { sdkConfiguration.site = newValue.sdkSite } + } + + /// The service name associated with data send to Datadog. + /// + /// Default value is set to application bundle identifier. + @objc public var service: String? { + get { sdkConfiguration.service } + set { sdkConfiguration.service = newValue } + } + + /// The preferred size of batched data uploaded to Datadog servers. + /// This value impacts the size and number of requests performed by the SDK. + /// + /// `.medium` by default. + @objc public var batchSize: DDBatchSize { + get { DDBatchSize(swiftType: sdkConfiguration.batchSize) } + set { sdkConfiguration.batchSize = newValue.swiftType } + } + + /// The preferred frequency of uploading data to Datadog servers. + /// This value impacts the frequency of performing network requests by the SDK. + /// + /// `.average` by default. + @objc public var uploadFrequency: DDUploadFrequency { + get { DDUploadFrequency(swiftType: sdkConfiguration.uploadFrequency) } + set { sdkConfiguration.uploadFrequency = newValue.swiftType } + } + + /// + @objc public var batchProcessingLevel: DDBatchProcessingLevel { + get { DDBatchProcessingLevel(swiftType: sdkConfiguration.batchProcessingLevel) } + set { sdkConfiguration.batchProcessingLevel = newValue.swiftType } + } + + /// Proxy configuration attributes. + /// This can be used to a enable a custom proxy for uploading tracked data to Datadog's intake. + @objc public var proxyConfiguration: [AnyHashable: Any]? { + get { sdkConfiguration.proxyConfiguration } + set { sdkConfiguration.proxyConfiguration = newValue } + } + + /// Sets Data encryption to use for on-disk data persistency by providing an object + /// complying with `DataEncryption` protocol. + @objc + public func setEncryption(_ encryption: DDDataEncryption) { + sdkConfiguration.encryption = DDDataEncryptionBridge(objcEncryption: encryption) + } + + /// A custom NTP synchronization interface. + /// + /// By default, the Datadog SDK synchronizes with dedicated NTP pools provided by the + /// https://www.ntppool.org/ . Using different pools or setting a no-op `ServerDateProvider` + /// implementation will result in desynchronization of the SDK instance and the Datadog servers. + /// This can lead to significant time shift in RUM sessions or distributed traces. + @objc + public func setServerDateProvider(_ serverDateProvider: DDServerDateProvider) { + sdkConfiguration.serverDateProvider = DDServerDateProviderBridge(objcProvider: serverDateProvider) + } + + /// The bundle object that contains the current executable. + @objc public var bundle: Bundle { + get { sdkConfiguration.bundle } + set { sdkConfiguration.bundle = newValue } + } + + /// Sets additional configuration attributes. + /// This can be used to tweak internal features of the SDK and shouldn't be considered as a part of public API. + @objc public var additionalConfiguration: [String: Any] { + get { sdkConfiguration._internal.additionalConfiguration } + set { sdkConfiguration._internal_mutation { $0.additionalConfiguration = newValue } } + } + + /// Flag that determines if UIApplication methods [`beginBackgroundTask(expirationHandler:)`](https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) and [`endBackgroundTask:`](https://developer.apple.com/documentation/uikit/uiapplication/1622970-endbackgroundtask) + /// are utilized to perform background uploads. It may extend the amount of time the app is operating in background by 30 seconds. + /// + /// Tasks are normally stopped when there's nothing to upload or when encountering any upload blocker such us no internet connection or low battery. + /// + /// `false` by default. + @objc public var backgroundTasksEnabled: Bool { + get { sdkConfiguration.backgroundTasksEnabled } + set { sdkConfiguration.backgroundTasksEnabled = newValue } + } + + /// Creates a Datadog SDK Configuration object. + /// + /// - Parameters: + /// - clientToken: Either the RUM client token (which supports RUM, Logging and APM) or regular client token, + /// only for Logging and APM. + /// + /// - env: The environment name which will be sent to Datadog. This can be used + /// To filter events on different environments (e.g. "staging" or "production"). + @objc + public init(clientToken: String, env: String) { + sdkConfiguration = .init(clientToken: clientToken, env: env) + } +} diff --git a/DatadogObjc/Sources/Logs/Logs+objc.swift b/DatadogObjc/Sources/Logs/Logs+objc.swift new file mode 100644 index 0000000000..d6f1c2d287 --- /dev/null +++ b/DatadogObjc/Sources/Logs/Logs+objc.swift @@ -0,0 +1,357 @@ +/* + * 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 DatadogLogs + +@objc +public enum DDSDKVerbosityLevel: Int { + case none + case debug + case warn + case error + case critical +} + +@objc +public enum DDLogLevel: Int { + case debug + case info + case notice + case warn + case error + case critical + + internal init(_ swift: LogLevel) { + 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 + } + } + + internal var swift: LogLevel { + 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 + } + } +} + +@objc +public class DDLogsConfiguration: NSObject { + internal var configuration: Logs.Configuration + + /// Overrides the custom server endpoint where Logs are sent. + @objc public var customEndpoint: URL? { + get { configuration.customEndpoint } + set { configuration.customEndpoint = newValue } + } + + /// Creates a Logs configuration object. + /// + /// - Parameters: + /// - customEndpoint: Overrides the custom server endpoint where Logs are sent. + @objc + public init( + customEndpoint: URL? = nil + ) { + configuration = .init( + customEndpoint: customEndpoint + ) + } + + /// Sets the custom mapper for `DDLogEvent`. This can be used to modify logs before they are send to Datadog. + /// + /// The implementation should obtain a mutable version of the `DDLogEvent`, modify it and return it. Returning `nil` will result + /// with dropping the Log event entirely, so it won't be send to Datadog. + @objc + public func setEventMapper(_ mapper: @escaping (DDLogEvent) -> DDLogEvent?) { + configuration.eventMapper = { swiftEvent in + let objcEvent = DDLogEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } +} + +@objc +public class DDLogs: NSObject { + @objc + public static func enable( + with configuration: DDLogsConfiguration = .init() + ) { + Logs.enable(with: configuration.configuration) + } + + @objc + public static func addAttribute(forKey key: String, value: Any) { + Logs.addAttribute(forKey: key, value: AnyEncodable(value)) + } + + @objc + public static func removeAttribute(forKey key: String) { + Logs.removeAttribute(forKey: key) + } +} + +@objc +public class DDLoggerConfiguration: NSObject { + internal var configuration: Logger.Configuration + + /// The service name (default value is set to application bundle identifier) + @objc public var service: String? { + get { configuration.service } + set { configuration.service = newValue } + } + + /// The logger custom name (default value is set to main bundle identifier) + @objc public var name: String? { + get { configuration.name } + set { configuration.name = newValue } + } + + /// Enriches logs with network connection info. + /// + /// This means: reachability status, connection type, mobile carrier name and many more will be added to each log. + /// For full list of network info attributes see `NetworkConnectionInfo` and `CarrierInfo`. + /// + /// `false` by default + @objc public var networkInfoEnabled: Bool { + get { configuration.networkInfoEnabled } + set { configuration.networkInfoEnabled = newValue } + } + + /// Enables the logs integration with RUM. + /// + /// If enabled all the logs will be enriched with the current RUM View information and + /// it will be possible to see all the logs sent during a specific View lifespan in the RUM Explorer. + /// + /// `true` by default + @objc public var bundleWithRumEnabled: Bool { + get { configuration.bundleWithRumEnabled } + set { configuration.bundleWithRumEnabled = newValue } + } + + /// Enables the logs integration with active span API from Tracing. + /// + /// If enabled all the logs will be bundled with the `DatadogTracer.shared().activeSpan` trace and + /// it will be possible to see all the logs sent during that specific trace. + /// + /// `true` by default + @objc public var bundleWithTraceEnabled: Bool { + get { configuration.bundleWithTraceEnabled } + set { configuration.bundleWithTraceEnabled = newValue } + } + + /// Sets the sampling rate for logging. + /// + /// The sampling rate must be a value between `0` and `100`. A value of `0` means no logs will be processed, `100` + /// means all logs will be processed. + /// + /// By default sampling is disabled, meaning that all logs are being processed). + @objc public var remoteSampleRate: Float { + get { configuration.remoteSampleRate } + set { configuration.remoteSampleRate = newValue } + } + + /// Enables logs to be printed to debugger console. + /// + /// `false` by default. + @objc public var printLogsToConsole: Bool { + get { configuration.consoleLogFormat != nil } + set { configuration.consoleLogFormat = newValue ? .short : nil } + } + + /// Set the minim log level reported to Datadog servers. + /// Any log with a level equal or above the threshold will be sent. + /// + /// Note: this setting doesn't impact logs printed to the console if `printLogsToConsole(_:)` + /// is used - all logs will be printed, no matter of their level. + /// + /// `DDLogLevel.debug` by default + @objc public var remoteLogThreshold: DDLogLevel { + get { DDLogLevel(configuration.remoteLogThreshold) } + set { configuration.remoteLogThreshold = newValue.swift } + } + + /// Creates a Logger Configuration. + /// + /// - Parameters: + /// - service: The service name (default value is set to application bundle identifier) + /// - name: The logger custom name (default value is set to main bundle identifier) + /// - networkInfoEnabled: Enriches logs with network connection info. `false` by default. + /// - bundleWithRumEnabled: Enables the logs integration with RUM. `true` by default. + /// - bundleWithTraceEnabled: Enables the logs integration with active span API from Tracing. `true` by default + /// - remoteSampleRate: The sample rate for remote logging. **When set to `0`, no log entries will be sent to Datadog servers.** + /// - remoteLogThreshold: Set the minimum log level reported to Datadog servers. .debug by default. + /// - printLogsToConsole: Format to use when printing logs to console - either `.short` or `.json`. + @objc + public init( + service: String? = nil, + name: String? = nil, + networkInfoEnabled: Bool = false, + bundleWithRumEnabled: Bool = true, + bundleWithTraceEnabled: Bool = true, + remoteSampleRate: SampleRate = .maxSampleRate, + remoteLogThreshold: DDLogLevel = .debug, + printLogsToConsole: Bool = false + ) { + configuration = .init( + service: service, + name: name, + networkInfoEnabled: networkInfoEnabled, + bundleWithRumEnabled: bundleWithRumEnabled, + bundleWithTraceEnabled: bundleWithTraceEnabled, + remoteSampleRate: remoteSampleRate, + remoteLogThreshold: remoteLogThreshold.swift, + consoleLogFormat: printLogsToConsole ? .short : nil + ) + } +} + +@objc +public class DDLogger: NSObject { + internal let sdkLogger: LoggerProtocol + + internal init(sdkLogger: LoggerProtocol) { + self.sdkLogger = sdkLogger + } + + // MARK: - Public + + @objc + public func debug(_ message: String) { + sdkLogger.debug(message) + } + + @objc + public func debug(_ message: String, attributes: [String: Any]) { + sdkLogger.debug(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func debug(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.debug(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func info(_ message: String) { + sdkLogger.info(message) + } + + @objc + public func info(_ message: String, attributes: [String: Any]) { + sdkLogger.info(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func info(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.info(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func notice(_ message: String) { + sdkLogger.notice(message) + } + + @objc + public func notice(_ message: String, attributes: [String: Any]) { + sdkLogger.notice(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func notice(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.notice(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func warn(_ message: String) { + sdkLogger.warn(message) + } + + @objc + public func warn(_ message: String, attributes: [String: Any]) { + sdkLogger.warn(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func warn(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.warn(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func error(_ message: String) { + sdkLogger.error(message) + } + + @objc + public func error(_ message: String, attributes: [String: Any]) { + sdkLogger.error(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func error(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.error(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func critical(_ message: String) { + sdkLogger.critical(message) + } + + @objc + public func critical(_ message: String, attributes: [String: Any]) { + sdkLogger.critical(message, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func critical(_ message: String, error: NSError, attributes: [String: Any]) { + sdkLogger.critical(message, error: error, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addAttribute(forKey key: String, value: Any) { + sdkLogger.addAttribute(forKey: key, value: AnyEncodable(value)) + } + + @objc + public func removeAttribute(forKey key: String) { + sdkLogger.removeAttribute(forKey: key) + } + + @objc + public func addTag(withKey key: String, value: String) { + sdkLogger.addTag(withKey: key, value: value) + } + + @objc + public func removeTag(withKey key: String) { + sdkLogger.removeTag(withKey: key) + } + + @objc + public func add(tag: String) { + sdkLogger.add(tag: tag) + } + + @objc + public func remove(tag: String) { + sdkLogger.remove(tag: tag) + } + + @objc + public static func create(with configuration: DDLoggerConfiguration = .init()) -> DDLogger { + return DDLogger(sdkLogger: Logger.create(with: configuration.configuration)) + } +} 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/Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift b/DatadogObjc/Sources/OpenTracing/OTSpan+objc.swift similarity index 80% rename from Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift rename to DatadogObjc/Sources/OpenTracing/OTSpan+objc.swift index 8f109f85a4..06de30ad36 100644 --- a/Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift +++ b/DatadogObjc/Sources/OpenTracing/OTSpan+objc.swift @@ -1,11 +1,13 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ -@objc +import Foundation + /// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTSpan.h +@objc public protocol OTSpan { var context: OTSpanContext { get } var tracer: OTTracer { get } @@ -22,6 +24,12 @@ public protocol OTSpan { func setBaggageItem(_ key: String, value: String) -> OTSpan func getBaggageItem(_ key: String) -> String? + func setError(_ error: Error) + func setError(kind: String, message: String, stack: String?) + func finish() func finishWithTime(_ finishTime: Date?) + + @discardableResult + func setActive() -> OTSpan } diff --git a/Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift b/DatadogObjc/Sources/OpenTracing/OTSpanContext+objc.swift similarity index 88% rename from Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift rename to DatadogObjc/Sources/OpenTracing/OTSpanContext+objc.swift index 3a28e32efb..addbe182de 100644 --- a/Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift +++ b/DatadogObjc/Sources/OpenTracing/OTSpanContext+objc.swift @@ -1,11 +1,13 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ -@objc +import Foundation + /// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTSpanContext.h +@objc public protocol OTSpanContext { func forEachBaggageItem(_ callback: (_ key: String, _ value: String) -> Bool) } diff --git a/Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift b/DatadogObjc/Sources/OpenTracing/OTTracer+objc.swift similarity index 85% rename from Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift rename to DatadogObjc/Sources/OpenTracing/OTTracer+objc.swift index bcd9a8e897..fb5dffae7f 100644 --- a/Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift +++ b/DatadogObjc/Sources/OpenTracing/OTTracer+objc.swift @@ -1,13 +1,18 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ -public let OTFormatHTTPHeaders = "OTFormatHTTPHeaders" +import Foundation @objc +public class OT: NSObject { + @objc public static let formatTextMap = "OTFormatTextMap" +} + /// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTTracer.h +@objc public protocol OTTracer { func startSpan(_ operationName: String) -> OTSpan func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan diff --git a/DatadogObjc/Sources/RUM/RUM+objc.swift b/DatadogObjc/Sources/RUM/RUM+objc.swift new file mode 100644 index 0000000000..26b651ee78 --- /dev/null +++ b/DatadogObjc/Sources/RUM/RUM+objc.swift @@ -0,0 +1,672 @@ +/* + * 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 DatadogInternal +import DatadogRUM + +internal struct UIKitRUMViewsPredicateBridge: UIKitRUMViewsPredicate { + let objcPredicate: DDUIKitRUMViewsPredicate + + func rumView(for viewController: UIViewController) -> RUMView? { + return objcPredicate.rumView(for: viewController)?.swiftView + } +} + +@objc +public class DDRUMView: NSObject { + let swiftView: RUMView + + @objc public var name: String { swiftView.name } + @objc public var attributes: [String: Any] { swiftView.attributes.dd.objCAttributes } + + /// Initializes the RUM View description. + /// - Parameters: + /// - name: the RUM View name, appearing as `VIEW NAME` in RUM Explorer. + /// - attributes: additional attributes to associate with the RUM View. + @objc + public init(name: String, attributes: [String: Any]) { + swiftView = RUMView( + name: name, + attributes: attributes.dd.swiftAttributes + ) + } +} + +@objc +public protocol DDUIKitRUMViewsPredicate: AnyObject { + /// The predicate deciding if the RUM View should be started or ended for given instance of the `UIViewController`. + /// - Parameter viewController: an instance of the view controller noticed by the SDK. + /// - Returns: RUM View parameters if received view controller should start/end the RUM View, `nil` otherwise. + func rumView(for viewController: UIViewController) -> DDRUMView? +} + +@objc +public class DDDefaultUIKitRUMViewsPredicate: NSObject, DDUIKitRUMViewsPredicate { + private let swiftPredicate = DefaultUIKitRUMViewsPredicate() + + public func rumView(for viewController: UIViewController) -> DDRUMView? { + return swiftPredicate.rumView(for: viewController).map { + DDRUMView(name: $0.name, attributes: $0.attributes.dd.objCAttributes) + } + } +} + +@objc +public class DDDefaultUIKitRUMActionsPredicate: NSObject, DDUIKitRUMActionsPredicate { + let swiftPredicate = DefaultUIKitRUMActionsPredicate() + #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: $0.attributes.dd.objCAttributes) + } + } + #else + public func rumAction(targetView: UIView) -> DDRUMAction? { + swiftPredicate.rumAction(targetView: targetView).map { + DDRUMAction(name: $0.name, attributes: $0.attributes.dd.objCAttributes) + } + } + #endif +} + +internal struct UIKitRUMActionsPredicateBridge: UITouchRUMActionsPredicate & UIPressRUMActionsPredicate { + let objcPredicate: AnyObject? + + init(objcPredicate: DDUITouchRUMActionsPredicate) { + self.objcPredicate = objcPredicate + } + + init(objcPredicate: DDUIPressRUMActionsPredicate) { + self.objcPredicate = objcPredicate + } + + func rumAction(targetView: UIView) -> RUMAction? { + guard let objcPredicate = objcPredicate as? DDUITouchRUMActionsPredicate else { + return nil + } + return objcPredicate.rumAction(targetView: targetView)?.swiftAction + } + + func rumAction(press type: UIPress.PressType, targetView: UIView) -> RUMAction? { + guard let objcPredicate = objcPredicate as? DDUIPressRUMActionsPredicate else { + return nil + } + return objcPredicate.rumAction(press: type, targetView: targetView)?.swiftAction + } +} + +@objc +public class DDRUMAction: NSObject { + let swiftAction: RUMAction + + @objc public var name: String { swiftAction.name } + @objc public var attributes: [String: Any] { swiftAction.attributes.dd.objCAttributes } + + /// Initializes the RUM Action description. + /// - Parameters: + /// - name: the RUM Action name, appearing as `ACTION NAME` in RUM Explorer. + /// - attributes: additional attributes to associate with the RUM Action. + @objc + public init(name: String, attributes: [String: Any]) { + swiftAction = RUMAction( + name: name, + attributes: attributes.dd.swiftAttributes + ) + } +} + +#if os(tvOS) +@objc +public protocol DDUIKitRUMActionsPredicate: DDUIPressRUMActionsPredicate {} +#else +@objc +public protocol DDUIKitRUMActionsPredicate: DDUITouchRUMActionsPredicate {} +#endif + +@objc +public protocol DDUITouchRUMActionsPredicate: AnyObject { + /// The predicate deciding if the RUM Action should be recorded. + /// - Parameter targetView: an instance of the `UIView` which received the action. + /// - Returns: RUM Action if it should be recorded, `nil` otherwise. + func rumAction(targetView: UIView) -> DDRUMAction? +} + +@objc +public protocol DDUIPressRUMActionsPredicate: AnyObject { + /// The predicate deciding if the RUM Action should be recorded. + /// - Parameters: + /// - type: the `UIPress.PressType` which received the action. + /// - targetView: an instance of the `UIView` which received the action. + /// - Returns: RUM Action if it should be recorded, `nil` otherwise. + func rumAction(press type: UIPress.PressType, targetView: UIView) -> DDRUMAction? +} + +@objc +public enum DDRUMErrorSource: Int { + /// Error originated in the source code. + case source + /// Error originated in the network layer. + case network + /// Error originated in a webview. + case webview + /// Error originated in a web console (used by bridges). + case console + /// Custom error source. + case custom + + internal var swiftType: RUMErrorSource { + switch self { + case .source: return .source + case .network: return .network + case .webview: return .webview + case .custom: return .custom + case .console: return .console + default: return .custom + } + } +} + +@objc +public enum DDRUMActionType: Int { + case tap + case scroll + case swipe + case custom + + internal var swiftType: RUMActionType { + switch self { + case .tap: return .tap + case .scroll: return .scroll + case .swipe: return .swipe + case .custom: return .custom + default: return .custom + } + } +} + +@objc +public enum DDRUMResourceType: Int { + case image + case xhr + case beacon + case css + case document + case fetch + case font + case js + case media + case other + case native + + internal var swiftType: RUMResourceType { + switch self { + case .image: return .image + case .xhr: return .xhr + case .beacon: return .beacon + case .css: return .css + case .document: return .document + case .fetch: return .fetch + case .font: return .font + case .js: return .js + case .media: return .media + case .native: return .native + default: return .other + } + } +} + +@objc +public enum DDRUMMethod: Int { + case post + case get + case head + case put + case delete + case patch + case connect + case trace + case options + + internal var swiftType: RUMMethod { + switch self { + case .post: return .post + case .get: return .get + case .head: return .head + case .put: return .put + case .delete: return .delete + case .patch: return .patch + case .connect: return .connect + case .trace: return .trace + case .options: return .options + default: return .get + } + } +} + +@objc +public enum DDRUMVitalsFrequency: Int { + case frequent + case average + case rare + case never + + internal init(swiftType: DatadogRUM.RUM.Configuration.VitalsFrequency?) { + switch swiftType { + case .frequent: self = .frequent + case .average: self = .average + case .rare: self = .rare + case .none: self = .never + } + } + + internal var swiftType: DatadogRUM.RUM.Configuration.VitalsFrequency? { + switch self { + case .frequent: return .frequent + case .average: return .average + case .rare: return .rare + case .never: return nil + } + } +} + +@objc +public class DDRUMFirstPartyHostsTracing: NSObject { + internal var swiftType: RUM.Configuration.URLSessionTracking.FirstPartyHostsTracing + + @objc + public init(hostsWithHeaderTypes: [String: Set]) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in Set(headerTypes.map { $0.swiftType }) } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders) + } + + @objc + public init(hostsWithHeaderTypes: [String: Set], sampleRate: Float) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in Set(headerTypes.map { $0.swiftType }) } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders, sampleRate: sampleRate) + } + + @objc + public init(hosts: Set) { + swiftType = .trace(hosts: hosts) + } + + @objc + public init(hosts: Set, sampleRate: Float) { + swiftType = .trace(hosts: hosts, sampleRate: sampleRate) + } +} + +@objc +public class DDRUMURLSessionTracking: NSObject { + internal var swiftConfig: RUM.Configuration.URLSessionTracking + + @objc + override public init() { + swiftConfig = .init() + } + + @objc + public func setFirstPartyHostsTracing(_ firstPartyHostsTracing: DDRUMFirstPartyHostsTracing) { + swiftConfig.firstPartyHostsTracing = firstPartyHostsTracing.swiftType + } + + @objc + 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?.dd.swiftAttributes + } + } +} + +@objc +public class DDRUMConfiguration: NSObject { + internal var swiftConfig: DatadogRUM.RUM.Configuration + + @objc + public init(applicationID: String) { + swiftConfig = .init(applicationID: applicationID) + } + + @objc public var applicationID: String { + swiftConfig.applicationID + } + + @objc public var sessionSampleRate: Float { + set { swiftConfig.sessionSampleRate = newValue } + get { swiftConfig.sessionSampleRate } + } + + @objc public var telemetrySampleRate: Float { + set { swiftConfig.telemetrySampleRate = newValue } + get { swiftConfig.telemetrySampleRate } + } + + @objc public var uiKitViewsPredicate: DDUIKitRUMViewsPredicate? { + set { swiftConfig.uiKitViewsPredicate = newValue.map { UIKitRUMViewsPredicateBridge(objcPredicate: $0) } } + get { (swiftConfig.uiKitViewsPredicate as? UIKitRUMViewsPredicateBridge)?.objcPredicate } + } + + @objc public var uiKitActionsPredicate: DDUIKitRUMActionsPredicate? { + set { swiftConfig.uiKitActionsPredicate = newValue.map { UIKitRUMActionsPredicateBridge(objcPredicate: $0) } } + get { (swiftConfig.uiKitActionsPredicate as? UIKitRUMActionsPredicateBridge)?.objcPredicate as? DDUIKitRUMActionsPredicate } + } + + @objc + public func setURLSessionTracking(_ tracking: DDRUMURLSessionTracking) { + swiftConfig.urlSessionTracking = tracking.swiftConfig + } + + @objc public var trackFrustrations: Bool { + set { swiftConfig.trackFrustrations = newValue } + get { swiftConfig.trackFrustrations } + } + + @objc public var trackBackgroundEvents: Bool { + set { swiftConfig.trackBackgroundEvents = newValue } + 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 == 0 ? nil : newValue } + get { swiftConfig.appHangThreshold ?? 0 } + } + + @objc public var vitalsUpdateFrequency: DDRUMVitalsFrequency { + set { swiftConfig.vitalsUpdateFrequency = newValue.swiftType } + get { DDRUMVitalsFrequency(swiftType: swiftConfig.vitalsUpdateFrequency) } + } + + @objc + public func setViewEventMapper(_ mapper: @escaping (DDRUMViewEvent) -> DDRUMViewEvent) { + swiftConfig.viewEventMapper = { swiftEvent in + let objcEvent = DDRUMViewEvent(swiftModel: swiftEvent) + return mapper(objcEvent).swiftModel + } + } + + @objc + public func setResourceEventMapper(_ mapper: @escaping (DDRUMResourceEvent) -> DDRUMResourceEvent?) { + swiftConfig.resourceEventMapper = { swiftEvent in + let objcEvent = DDRUMResourceEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } + + @objc + public func setActionEventMapper(_ mapper: @escaping (DDRUMActionEvent) -> DDRUMActionEvent?) { + swiftConfig.actionEventMapper = { swiftEvent in + let objcEvent = DDRUMActionEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } + + @objc + public func setErrorEventMapper(_ mapper: @escaping (DDRUMErrorEvent) -> DDRUMErrorEvent?) { + swiftConfig.errorEventMapper = { swiftEvent in + let objcEvent = DDRUMErrorEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } + + @objc + public func setLongTaskEventMapper(_ mapper: @escaping (DDRUMLongTaskEvent) -> DDRUMLongTaskEvent?) { + swiftConfig.longTaskEventMapper = { swiftEvent in + let objcEvent = DDRUMLongTaskEvent(swiftModel: swiftEvent) + return mapper(objcEvent)?.swiftModel + } + } + + @objc public var onSessionStart: ((String, Bool) -> Void)? { + set { swiftConfig.onSessionStart = newValue } + get { swiftConfig.onSessionStart } + } + + @objc public var customEndpoint: URL? { + set { swiftConfig.customEndpoint = newValue } + get { swiftConfig.customEndpoint } + } +} + +@objc +public class DDRUM: NSObject { + @objc + public static func enable(with configuration: DDRUMConfiguration) { + RUM.enable(with: configuration.swiftConfig) + } +} + +@objc +public class DDRUMMonitor: NSObject { + // MARK: - Internal + + internal let swiftRUMMonitor: DatadogRUM.RUMMonitorProtocol + + internal init(swiftRUMMonitor: DatadogRUM.RUMMonitorProtocol) { + self.swiftRUMMonitor = swiftRUMMonitor + } + + // MARK: - Public + + @objc + public static func shared() -> DDRUMMonitor { + DDRUMMonitor(swiftRUMMonitor: RUMMonitor.shared()) + } + + @objc + public func currentSessionID(completion: @escaping (String?) -> Void) { + swiftRUMMonitor.currentSessionID(completion: completion) + } + + @objc + public func stopSession() { + swiftRUMMonitor.stopSession() + } + + @objc + public func startView( + viewController: UIViewController, + name: String?, + attributes: [String: Any] + ) { + swiftRUMMonitor.startView(viewController: viewController, name: name, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopView( + viewController: UIViewController, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopView(viewController: viewController, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func startView( + key: String, + name: String?, + attributes: [String: Any] + ) { + swiftRUMMonitor.startView(key: key, name: name, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopView( + key: String, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopView(key: key, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addTiming(name: String) { + swiftRUMMonitor.addTiming(name: name) + } + + @objc + public func addError( + message: String, + stack: String?, + source: DDRUMErrorSource, + attributes: [String: Any] + ) { + swiftRUMMonitor.addError(message: message, stack: stack, source: source.swiftType, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addError( + error: Error, + source: DDRUMErrorSource, + attributes: [String: Any] + ) { + swiftRUMMonitor.addError(error: error, source: source.swiftType, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func startResource( + resourceKey: String, + request: URLRequest, + attributes: [String: Any] + ) { + swiftRUMMonitor.startResource(resourceKey: resourceKey, request: request, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func startResource( + resourceKey: String, + url: URL, + attributes: [String: Any] + ) { + swiftRUMMonitor.startResource(resourceKey: resourceKey, url: url, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func startResource( + resourceKey: String, + httpMethod: DDRUMMethod, + urlString: String, + attributes: [String: Any] + ) { + swiftRUMMonitor.startResource(resourceKey: resourceKey, httpMethod: httpMethod.swiftType, urlString: urlString, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addResourceMetrics( + resourceKey: String, + metrics: URLSessionTaskMetrics, + attributes: [String: Any] + ) { + swiftRUMMonitor.addResourceMetrics(resourceKey: resourceKey, metrics: metrics, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopResource( + resourceKey: String, + response: URLResponse, + size: NSNumber?, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopResource(resourceKey: resourceKey, response: response, size: size?.int64Value, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopResource( + resourceKey: String, + statusCode: NSNumber?, + kind: DDRUMResourceType, + size: NSNumber?, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopResource( + resourceKey: resourceKey, + statusCode: statusCode?.intValue, + kind: kind.swiftType, + size: size?.int64Value, + attributes: attributes.dd.swiftAttributes + ) + } + + @objc + public func stopResourceWithError( + resourceKey: String, + error: Error, + response: URLResponse?, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, error: error, response: response, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopResourceWithError( + resourceKey: String, + message: String, + response: URLResponse?, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopResourceWithError(resourceKey: resourceKey, message: message, response: response, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func startAction( + type: DDRUMActionType, + name: String, + attributes: [String: Any] + ) { + swiftRUMMonitor.startAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func stopAction( + type: DDRUMActionType, + name: String?, + attributes: [String: Any] + ) { + swiftRUMMonitor.stopAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addAction( + type: DDRUMActionType, + name: String, + attributes: [String: Any] + ) { + swiftRUMMonitor.addAction(type: type.swiftType, name: name, attributes: attributes.dd.swiftAttributes) + } + + @objc + public func addAttribute( + forKey key: String, + value: Any + ) { + swiftRUMMonitor.addAttribute(forKey: key, value: AnyEncodable(value)) + } + + @objc + public func removeAttribute(forKey key: String) { + swiftRUMMonitor.removeAttribute(forKey: key) + } + + @objc + public func addFeatureFlagEvaluation(name: String, value: Any) { + swiftRUMMonitor.addFeatureFlagEvaluation(name: name, value: AnyEncodable(value)) + } + + @objc public var debug: Bool { + set { swiftRUMMonitor.debug = newValue } + get { swiftRUMMonitor.debug } + } +} diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift new file mode 100644 index 0000000000..a39cf11248 --- /dev/null +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -0,0 +1,8065 @@ +/* + * 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 DatadogRUM + +// This file was generated from JSON Schema. Do not modify it directly. + +// swiftlint:disable force_unwrapping + +@objc +public class DDRUMActionEvent: NSObject { + internal var swiftModel: RUMActionEvent + internal var root: DDRUMActionEvent { self } + + internal init(swiftModel: RUMActionEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMActionEventDD { + DDRUMActionEventDD(root: root) + } + + @objc public var action: DDRUMActionEventAction { + DDRUMActionEventAction(root: root) + } + + @objc public var application: DDRUMActionEventApplication { + DDRUMActionEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMActionEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMActionEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMActionEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMActionEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMActionEventContainer? { + root.swiftModel.container != nil ? DDRUMActionEventContainer(root: root) : nil + } + + @objc public var context: DDRUMActionEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMActionEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMActionEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMActionEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMActionEventDisplay? { + root.swiftModel.display != nil ? DDRUMActionEventDisplay(root: root) : nil + } + + @objc public var os: DDRUMActionEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMActionEventRUMOperatingSystem(root: root) : nil + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMActionEventSession { + DDRUMActionEventSession(root: root) + } + + @objc public var source: DDRUMActionEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMActionEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMActionEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMActionEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMActionEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMActionEventView { + DDRUMActionEventView(root: root) + } +} + +@objc +public class DDRUMActionEventDD: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var action: DDRUMActionEventDDAction? { + root.swiftModel.dd.action != nil ? DDRUMActionEventDDAction(root: root) : nil + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMActionEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMActionEventDDConfiguration(root: root) : nil + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var session: DDRUMActionEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMActionEventDDSession(root: root) : nil + } +} + +@objc +public class DDRUMActionEventDDAction: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + 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 + } + + @objc public var target: DDRUMActionEventDDActionTarget? { + root.swiftModel.dd.action!.target != nil ? DDRUMActionEventDDActionTarget(root: root) : nil + } +} + +@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 + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var x: NSNumber { + root.swiftModel.dd.action!.position!.x as NSNumber + } + + @objc public var y: NSNumber { + root.swiftModel.dd.action!.position!.y as NSNumber + } +} + +@objc +public class DDRUMActionEventDDActionTarget: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var height: NSNumber? { + root.swiftModel.dd.action!.target!.height as NSNumber? + } + + @objc public var selector: String? { + root.swiftModel.dd.action!.target!.selector + } + + @objc public var width: NSNumber? { + root.swiftModel.dd.action!.target!.width as NSNumber? + } +} + +@objc +public class DDRUMActionEventDDConfiguration: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } +} + +@objc +public class DDRUMActionEventDDSession: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var plan: DDRUMActionEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMActionEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMActionEventDDSessionPlan: Int { + internal init(swift: RUMActionEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMActionEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMActionEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMActionEventAction: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var crash: DDRUMActionEventActionCrash? { + root.swiftModel.action.crash != nil ? DDRUMActionEventActionCrash(root: root) : nil + } + + @objc public var error: DDRUMActionEventActionError? { + root.swiftModel.action.error != nil ? DDRUMActionEventActionError(root: root) : nil + } + + @objc public var frustration: DDRUMActionEventActionFrustration? { + root.swiftModel.action.frustration != nil ? DDRUMActionEventActionFrustration(root: root) : nil + } + + @objc public var id: String? { + root.swiftModel.action.id + } + + @objc public var loadingTime: NSNumber? { + root.swiftModel.action.loadingTime as NSNumber? + } + + @objc public var longTask: DDRUMActionEventActionLongTask? { + root.swiftModel.action.longTask != nil ? DDRUMActionEventActionLongTask(root: root) : nil + } + + @objc public var resource: DDRUMActionEventActionResource? { + root.swiftModel.action.resource != nil ? DDRUMActionEventActionResource(root: root) : nil + } + + @objc public var target: DDRUMActionEventActionTarget? { + root.swiftModel.action.target != nil ? DDRUMActionEventActionTarget(root: root) : nil + } + + @objc public var type: DDRUMActionEventActionActionType { + .init(swift: root.swiftModel.action.type) + } +} + +@objc +public class DDRUMActionEventActionCrash: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.action.crash!.count as NSNumber + } +} + +@objc +public class DDRUMActionEventActionError: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.action.error!.count as NSNumber + } +} + +@objc +public class DDRUMActionEventActionFrustration: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var type: [Int] { + root.swiftModel.action.frustration!.type.map { DDRUMActionEventActionFrustrationFrustrationType(swift: $0).rawValue } + } +} + +@objc +public enum DDRUMActionEventActionFrustrationFrustrationType: Int { + internal init(swift: RUMActionEvent.Action.Frustration.FrustrationType) { + switch swift { + case .rageClick: self = .rageClick + case .deadClick: self = .deadClick + case .errorClick: self = .errorClick + case .rageTap: self = .rageTap + case .errorTap: self = .errorTap + } + } + + internal var toSwift: RUMActionEvent.Action.Frustration.FrustrationType { + switch self { + case .rageClick: return .rageClick + case .deadClick: return .deadClick + case .errorClick: return .errorClick + case .rageTap: return .rageTap + case .errorTap: return .errorTap + } + } + + case rageClick + case deadClick + case errorClick + case rageTap + case errorTap +} + +@objc +public class DDRUMActionEventActionLongTask: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.action.longTask!.count as NSNumber + } +} + +@objc +public class DDRUMActionEventActionResource: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.action.resource!.count as NSNumber + } +} + +@objc +public class DDRUMActionEventActionTarget: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var name: String { + set { root.swiftModel.action.target!.name = newValue } + get { root.swiftModel.action.target!.name } + } +} + +@objc +public enum DDRUMActionEventActionActionType: Int { + internal init(swift: RUMActionEvent.Action.ActionType) { + switch swift { + case .custom: self = .custom + case .click: self = .click + case .tap: self = .tap + case .scroll: self = .scroll + case .swipe: self = .swipe + case .applicationStart: self = .applicationStart + case .back: self = .back + } + } + + internal var toSwift: RUMActionEvent.Action.ActionType { + switch self { + case .custom: return .custom + case .click: return .click + case .tap: return .tap + case .scroll: return .scroll + case .swipe: return .swipe + case .applicationStart: return .applicationStart + case .back: return .back + } + } + + case custom + case click + case tap + case scroll + case swipe + case applicationStart + case back +} + +@objc +public class DDRUMActionEventApplication: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMActionEventRUMCITest: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMActionEventRUMConnectivity: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var cellular: DDRUMActionEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMActionEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMActionEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMActionEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMActionEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMActionEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMActionEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMActionEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMActionEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMActionEventContainer: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var source: DDRUMActionEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMActionEventContainerView { + DDRUMActionEventContainerView(root: root) + } +} + +@objc +public enum DDRUMActionEventContainerSource: Int { + internal init(swift: RUMActionEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMActionEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMActionEventContainerView: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMActionEventRUMEventAttributes: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMActionEventRUMDevice: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMActionEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMActionEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMActionEventDisplay: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var viewport: DDRUMActionEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMActionEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMActionEventDisplayViewport: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMActionEventRUMOperatingSystem: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMActionEventSession: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var type: DDRUMActionEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMActionEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMActionEventSource: Int { + internal init(swift: RUMActionEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMActionEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMActionEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMActionEventRUMUser: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMActionEventView: NSObject { + internal let root: DDRUMActionEvent + + internal init(root: DDRUMActionEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var inForeground: NSNumber? { + root.swiftModel.view.inForeground as NSNumber? + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + + @objc public var referrer: String? { + set { root.swiftModel.view.referrer = newValue } + get { root.swiftModel.view.referrer } + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMErrorEvent: NSObject { + internal var swiftModel: RUMErrorEvent + internal var root: DDRUMErrorEvent { self } + + internal init(swiftModel: RUMErrorEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMErrorEventDD { + DDRUMErrorEventDD(root: root) + } + + @objc public var action: DDRUMErrorEventAction? { + root.swiftModel.action != nil ? DDRUMErrorEventAction(root: root) : nil + } + + @objc public var application: DDRUMErrorEventApplication { + DDRUMErrorEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMErrorEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMErrorEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMErrorEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMErrorEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMErrorEventContainer? { + root.swiftModel.container != nil ? DDRUMErrorEventContainer(root: root) : nil + } + + @objc public var context: DDRUMErrorEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMErrorEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMErrorEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMErrorEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMErrorEventDisplay? { + root.swiftModel.display != nil ? DDRUMErrorEventDisplay(root: root) : nil + } + + @objc public var error: DDRUMErrorEventError { + DDRUMErrorEventError(root: root) + } + + @objc public var featureFlags: DDRUMErrorEventFeatureFlags? { + root.swiftModel.featureFlags != nil ? DDRUMErrorEventFeatureFlags(root: root) : nil + } + + @objc public var freeze: DDRUMErrorEventFreeze? { + root.swiftModel.freeze != nil ? DDRUMErrorEventFreeze(root: root) : nil + } + + @objc public var os: DDRUMErrorEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMErrorEventRUMOperatingSystem(root: root) : nil + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMErrorEventSession { + DDRUMErrorEventSession(root: root) + } + + @objc public var source: DDRUMErrorEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMErrorEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMErrorEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMErrorEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMErrorEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMErrorEventView { + DDRUMErrorEventView(root: root) + } +} + +@objc +public class DDRUMErrorEventDD: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMErrorEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMErrorEventDDConfiguration(root: root) : nil + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var session: DDRUMErrorEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMErrorEventDDSession(root: root) : nil + } +} + +@objc +public class DDRUMErrorEventDDConfiguration: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } +} + +@objc +public class DDRUMErrorEventDDSession: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var plan: DDRUMErrorEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMErrorEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMErrorEventDDSessionPlan: Int { + internal init(swift: RUMErrorEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMErrorEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMErrorEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMErrorEventAction: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var id: DDRUMErrorEventActionRUMActionID { + DDRUMErrorEventActionRUMActionID(root: root) + } +} + +@objc +public class DDRUMErrorEventActionRUMActionID: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var string: String? { + guard case .string(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } + + @objc public var stringsArray: [String]? { + guard case .stringsArray(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } +} + +@objc +public class DDRUMErrorEventApplication: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMErrorEventRUMCITest: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMErrorEventRUMConnectivity: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var cellular: DDRUMErrorEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMErrorEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMErrorEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMErrorEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMErrorEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMErrorEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMErrorEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMErrorEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMErrorEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMErrorEventContainer: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var source: DDRUMErrorEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMErrorEventContainerView { + DDRUMErrorEventContainerView(root: root) + } +} + +@objc +public enum DDRUMErrorEventContainerSource: Int { + internal init(swift: RUMErrorEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMErrorEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMErrorEventContainerView: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMErrorEventRUMEventAttributes: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMErrorEventRUMDevice: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMErrorEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMErrorEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMErrorEventDisplay: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var viewport: DDRUMErrorEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMErrorEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMErrorEventDisplayViewport: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMErrorEventError: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var binaryImages: [DDRUMErrorEventErrorBinaryImages]? { + root.swiftModel.error.binaryImages?.map { DDRUMErrorEventErrorBinaryImages(swiftModel: $0) } + } + + @objc public var category: DDRUMErrorEventErrorCategory { + .init(swift: root.swiftModel.error.category) + } + + @objc public var causes: [DDRUMErrorEventErrorCauses]? { + set { root.swiftModel.error.causes = newValue?.map { $0.swiftModel } } + get { root.swiftModel.error.causes?.map { DDRUMErrorEventErrorCauses(swiftModel: $0) } } + } + + @objc public var csp: DDRUMErrorEventErrorCSP? { + root.swiftModel.error.csp != nil ? DDRUMErrorEventErrorCSP(root: root) : nil + } + + @objc public var fingerprint: String? { + set { root.swiftModel.error.fingerprint = newValue } + get { root.swiftModel.error.fingerprint } + } + + @objc public var handling: DDRUMErrorEventErrorHandling { + .init(swift: root.swiftModel.error.handling) + } + + @objc public var handlingStack: String? { + root.swiftModel.error.handlingStack + } + + @objc public var id: String? { + root.swiftModel.error.id + } + + @objc public var isCrash: NSNumber? { + root.swiftModel.error.isCrash as NSNumber? + } + + @objc public var message: String { + set { root.swiftModel.error.message = newValue } + get { root.swiftModel.error.message } + } + + @objc public var meta: DDRUMErrorEventErrorMeta? { + root.swiftModel.error.meta != nil ? DDRUMErrorEventErrorMeta(root: root) : nil + } + + @objc public var resource: DDRUMErrorEventErrorResource? { + root.swiftModel.error.resource != nil ? DDRUMErrorEventErrorResource(root: root) : nil + } + + @objc public var source: DDRUMErrorEventErrorSource { + .init(swift: root.swiftModel.error.source) + } + + @objc public var sourceType: DDRUMErrorEventErrorSourceType { + .init(swift: root.swiftModel.error.sourceType) + } + + @objc public var stack: String? { + set { root.swiftModel.error.stack = newValue } + get { root.swiftModel.error.stack } + } + + @objc public var threads: [DDRUMErrorEventErrorThreads]? { + root.swiftModel.error.threads?.map { DDRUMErrorEventErrorThreads(swiftModel: $0) } + } + + @objc public var timeSinceAppStart: NSNumber? { + root.swiftModel.error.timeSinceAppStart as NSNumber? + } + + @objc public var type: String? { + root.swiftModel.error.type + } + + @objc public var wasTruncated: NSNumber? { + root.swiftModel.error.wasTruncated as NSNumber? + } +} + +@objc +public class DDRUMErrorEventErrorBinaryImages: NSObject { + internal var swiftModel: RUMErrorEvent.Error.BinaryImages + internal var root: DDRUMErrorEventErrorBinaryImages { self } + + internal init(swiftModel: RUMErrorEvent.Error.BinaryImages) { + self.swiftModel = swiftModel + } + + @objc public var arch: String? { + root.swiftModel.arch + } + + @objc public var isSystem: NSNumber { + root.swiftModel.isSystem as NSNumber + } + + @objc public var loadAddress: String? { + root.swiftModel.loadAddress + } + + @objc public var maxAddress: String? { + root.swiftModel.maxAddress + } + + @objc public var name: String { + root.swiftModel.name + } + + @objc public var uuid: String { + root.swiftModel.uuid + } +} + +@objc +public enum DDRUMErrorEventErrorCategory: Int { + internal init(swift: RUMErrorEvent.Error.Category?) { + switch swift { + case nil: self = .none + case .aNR?: self = .aNR + case .appHang?: self = .appHang + case .exception?: self = .exception + case .watchdogTermination?: self = .watchdogTermination + case .memoryWarning?: self = .memoryWarning + } + } + + internal var toSwift: RUMErrorEvent.Error.Category? { + switch self { + case .none: return nil + case .aNR: return .aNR + case .appHang: return .appHang + case .exception: return .exception + case .watchdogTermination: return .watchdogTermination + case .memoryWarning: return .memoryWarning + } + } + + case none + case aNR + case appHang + case exception + case watchdogTermination + case memoryWarning +} + +@objc +public class DDRUMErrorEventErrorCauses: NSObject { + internal var swiftModel: RUMErrorEvent.Error.Causes + internal var root: DDRUMErrorEventErrorCauses { self } + + internal init(swiftModel: RUMErrorEvent.Error.Causes) { + self.swiftModel = swiftModel + } + + @objc public var message: String { + set { root.swiftModel.message = newValue } + get { root.swiftModel.message } + } + + @objc public var source: DDRUMErrorEventErrorCausesSource { + .init(swift: root.swiftModel.source) + } + + @objc public var stack: String? { + set { root.swiftModel.stack = newValue } + get { root.swiftModel.stack } + } + + @objc public var type: String? { + root.swiftModel.type + } +} + +@objc +public enum DDRUMErrorEventErrorCausesSource: Int { + internal init(swift: RUMErrorEvent.Error.Causes.Source) { + switch swift { + case .network: self = .network + case .source: self = .source + case .console: self = .console + case .logger: self = .logger + case .agent: self = .agent + case .webview: self = .webview + case .custom: self = .custom + case .report: self = .report + } + } + + internal var toSwift: RUMErrorEvent.Error.Causes.Source { + switch self { + case .network: return .network + case .source: return .source + case .console: return .console + case .logger: return .logger + case .agent: return .agent + case .webview: return .webview + case .custom: return .custom + case .report: return .report + } + } + + case network + case source + case console + case logger + case agent + case webview + case custom + case report +} + +@objc +public class DDRUMErrorEventErrorCSP: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var disposition: DDRUMErrorEventErrorCSPDisposition { + .init(swift: root.swiftModel.error.csp!.disposition) + } +} + +@objc +public enum DDRUMErrorEventErrorCSPDisposition: Int { + internal init(swift: RUMErrorEvent.Error.CSP.Disposition?) { + switch swift { + case nil: self = .none + case .enforce?: self = .enforce + case .report?: self = .report + } + } + + internal var toSwift: RUMErrorEvent.Error.CSP.Disposition? { + switch self { + case .none: return nil + case .enforce: return .enforce + case .report: return .report + } + } + + case none + case enforce + case report +} + +@objc +public enum DDRUMErrorEventErrorHandling: Int { + internal init(swift: RUMErrorEvent.Error.Handling?) { + switch swift { + case nil: self = .none + case .handled?: self = .handled + case .unhandled?: self = .unhandled + } + } + + internal var toSwift: RUMErrorEvent.Error.Handling? { + switch self { + case .none: return nil + case .handled: return .handled + case .unhandled: return .unhandled + } + } + + case none + case handled + case unhandled +} + +@objc +public class DDRUMErrorEventErrorMeta: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var codeType: String? { + root.swiftModel.error.meta!.codeType + } + + @objc public var exceptionCodes: String? { + root.swiftModel.error.meta!.exceptionCodes + } + + @objc public var exceptionType: String? { + root.swiftModel.error.meta!.exceptionType + } + + @objc public var incidentIdentifier: String? { + root.swiftModel.error.meta!.incidentIdentifier + } + + @objc public var parentProcess: String? { + root.swiftModel.error.meta!.parentProcess + } + + @objc public var path: String? { + root.swiftModel.error.meta!.path + } + + @objc public var process: String? { + root.swiftModel.error.meta!.process + } +} + +@objc +public class DDRUMErrorEventErrorResource: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var method: DDRUMErrorEventErrorResourceRUMMethod { + .init(swift: root.swiftModel.error.resource!.method) + } + + @objc public var provider: DDRUMErrorEventErrorResourceProvider? { + root.swiftModel.error.resource!.provider != nil ? DDRUMErrorEventErrorResourceProvider(root: root) : nil + } + + @objc public var statusCode: NSNumber { + root.swiftModel.error.resource!.statusCode as NSNumber + } + + @objc public var url: String { + set { root.swiftModel.error.resource!.url = newValue } + get { root.swiftModel.error.resource!.url } + } +} + +@objc +public enum DDRUMErrorEventErrorResourceRUMMethod: Int { + internal init(swift: RUMMethod) { + switch swift { + case .post: self = .post + case .get: self = .get + case .head: self = .head + case .put: self = .put + case .delete: self = .delete + case .patch: self = .patch + case .trace: self = .trace + case .options: self = .options + case .connect: self = .connect + } + } + + internal var toSwift: RUMMethod { + switch self { + case .post: return .post + case .get: return .get + case .head: return .head + case .put: return .put + case .delete: return .delete + case .patch: return .patch + case .trace: return .trace + case .options: return .options + case .connect: return .connect + } + } + + case post + case get + case head + case put + case delete + case patch + case trace + case options + case connect +} + +@objc +public class DDRUMErrorEventErrorResourceProvider: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var domain: String? { + root.swiftModel.error.resource!.provider!.domain + } + + @objc public var name: String? { + root.swiftModel.error.resource!.provider!.name + } + + @objc public var type: DDRUMErrorEventErrorResourceProviderProviderType { + .init(swift: root.swiftModel.error.resource!.provider!.type) + } +} + +@objc +public enum DDRUMErrorEventErrorResourceProviderProviderType: Int { + internal init(swift: RUMErrorEvent.Error.Resource.Provider.ProviderType?) { + switch swift { + case nil: self = .none + case .ad?: self = .ad + case .advertising?: self = .advertising + case .analytics?: self = .analytics + case .cdn?: self = .cdn + case .content?: self = .content + case .customerSuccess?: self = .customerSuccess + case .firstParty?: self = .firstParty + case .hosting?: self = .hosting + case .marketing?: self = .marketing + case .other?: self = .other + case .social?: self = .social + case .tagManager?: self = .tagManager + case .utility?: self = .utility + case .video?: self = .video + } + } + + internal var toSwift: RUMErrorEvent.Error.Resource.Provider.ProviderType? { + switch self { + case .none: return nil + case .ad: return .ad + case .advertising: return .advertising + case .analytics: return .analytics + case .cdn: return .cdn + case .content: return .content + case .customerSuccess: return .customerSuccess + case .firstParty: return .firstParty + case .hosting: return .hosting + case .marketing: return .marketing + case .other: return .other + case .social: return .social + case .tagManager: return .tagManager + case .utility: return .utility + case .video: return .video + } + } + + case none + case ad + case advertising + case analytics + case cdn + case content + case customerSuccess + case firstParty + case hosting + case marketing + case other + case social + case tagManager + case utility + case video +} + +@objc +public enum DDRUMErrorEventErrorSource: Int { + internal init(swift: RUMErrorEvent.Error.Source) { + switch swift { + case .network: self = .network + case .source: self = .source + case .console: self = .console + case .logger: self = .logger + case .agent: self = .agent + case .webview: self = .webview + case .custom: self = .custom + case .report: self = .report + } + } + + internal var toSwift: RUMErrorEvent.Error.Source { + switch self { + case .network: return .network + case .source: return .source + case .console: return .console + case .logger: return .logger + case .agent: return .agent + case .webview: return .webview + case .custom: return .custom + case .report: return .report + } + } + + case network + case source + case console + case logger + case agent + case webview + case custom + case report +} + +@objc +public enum DDRUMErrorEventErrorSourceType: Int { + internal init(swift: RUMErrorEvent.Error.SourceType?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .browser?: self = .browser + case .ios?: self = .ios + case .reactNative?: self = .reactNative + case .flutter?: self = .flutter + case .roku?: self = .roku + case .ndk?: self = .ndk + case .iosIl2cpp?: self = .iosIl2cpp + case .ndkIl2cpp?: self = .ndkIl2cpp + } + } + + internal var toSwift: RUMErrorEvent.Error.SourceType? { + switch self { + case .none: return nil + case .android: return .android + case .browser: return .browser + case .ios: return .ios + case .reactNative: return .reactNative + case .flutter: return .flutter + case .roku: return .roku + case .ndk: return .ndk + case .iosIl2cpp: return .iosIl2cpp + case .ndkIl2cpp: return .ndkIl2cpp + } + } + + case none + case android + case browser + case ios + case reactNative + case flutter + case roku + case ndk + case iosIl2cpp + case ndkIl2cpp +} + +@objc +public class DDRUMErrorEventErrorThreads: NSObject { + internal var swiftModel: RUMErrorEvent.Error.Threads + internal var root: DDRUMErrorEventErrorThreads { self } + + internal init(swiftModel: RUMErrorEvent.Error.Threads) { + self.swiftModel = swiftModel + } + + @objc public var crashed: NSNumber { + root.swiftModel.crashed as NSNumber + } + + @objc public var name: String { + root.swiftModel.name + } + + @objc public var stack: String { + root.swiftModel.stack + } + + @objc public var state: String? { + root.swiftModel.state + } +} + +@objc +public class DDRUMErrorEventFeatureFlags: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var featureFlagsInfo: [String: Any] { + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMErrorEventFreeze: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.freeze!.duration as NSNumber + } +} + +@objc +public class DDRUMErrorEventRUMOperatingSystem: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMErrorEventSession: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var type: DDRUMErrorEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMErrorEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMErrorEventSource: Int { + internal init(swift: RUMErrorEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMErrorEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMErrorEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMErrorEventRUMUser: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMErrorEventView: NSObject { + internal let root: DDRUMErrorEvent + + internal init(root: DDRUMErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var inForeground: NSNumber? { + root.swiftModel.view.inForeground as NSNumber? + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + + @objc public var referrer: String? { + set { root.swiftModel.view.referrer = newValue } + get { root.swiftModel.view.referrer } + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMLongTaskEvent: NSObject { + internal var swiftModel: RUMLongTaskEvent + internal var root: DDRUMLongTaskEvent { self } + + internal init(swiftModel: RUMLongTaskEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMLongTaskEventDD { + DDRUMLongTaskEventDD(root: root) + } + + @objc public var action: DDRUMLongTaskEventAction? { + root.swiftModel.action != nil ? DDRUMLongTaskEventAction(root: root) : nil + } + + @objc public var application: DDRUMLongTaskEventApplication { + DDRUMLongTaskEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMLongTaskEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMLongTaskEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMLongTaskEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMLongTaskEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMLongTaskEventContainer? { + root.swiftModel.container != nil ? DDRUMLongTaskEventContainer(root: root) : nil + } + + @objc public var context: DDRUMLongTaskEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMLongTaskEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMLongTaskEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMLongTaskEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMLongTaskEventDisplay? { + root.swiftModel.display != nil ? DDRUMLongTaskEventDisplay(root: root) : nil + } + + @objc public var longTask: DDRUMLongTaskEventLongTask { + DDRUMLongTaskEventLongTask(root: root) + } + + @objc public var os: DDRUMLongTaskEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMLongTaskEventRUMOperatingSystem(root: root) : nil + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMLongTaskEventSession { + DDRUMLongTaskEventSession(root: root) + } + + @objc public var source: DDRUMLongTaskEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMLongTaskEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMLongTaskEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMLongTaskEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMLongTaskEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMLongTaskEventView { + DDRUMLongTaskEventView(root: root) + } +} + +@objc +public class DDRUMLongTaskEventDD: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMLongTaskEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMLongTaskEventDDConfiguration(root: root) : nil + } + + @objc public var discarded: NSNumber? { + root.swiftModel.dd.discarded as NSNumber? + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var session: DDRUMLongTaskEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMLongTaskEventDDSession(root: root) : nil + } +} + +@objc +public class DDRUMLongTaskEventDDConfiguration: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } +} + +@objc +public class DDRUMLongTaskEventDDSession: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var plan: DDRUMLongTaskEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMLongTaskEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMLongTaskEventDDSessionPlan: Int { + internal init(swift: RUMLongTaskEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMLongTaskEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMLongTaskEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMLongTaskEventAction: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var id: DDRUMLongTaskEventActionRUMActionID { + DDRUMLongTaskEventActionRUMActionID(root: root) + } +} + +@objc +public class DDRUMLongTaskEventActionRUMActionID: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var string: String? { + guard case .string(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } + + @objc public var stringsArray: [String]? { + guard case .stringsArray(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } +} + +@objc +public class DDRUMLongTaskEventApplication: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMLongTaskEventRUMCITest: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMLongTaskEventRUMConnectivity: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var cellular: DDRUMLongTaskEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMLongTaskEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMLongTaskEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMLongTaskEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMLongTaskEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMLongTaskEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMLongTaskEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMLongTaskEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMLongTaskEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMLongTaskEventContainer: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var source: DDRUMLongTaskEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMLongTaskEventContainerView { + DDRUMLongTaskEventContainerView(root: root) + } +} + +@objc +public enum DDRUMLongTaskEventContainerSource: Int { + internal init(swift: RUMLongTaskEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMLongTaskEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMLongTaskEventContainerView: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMLongTaskEventRUMEventAttributes: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMLongTaskEventRUMDevice: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMLongTaskEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMLongTaskEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMLongTaskEventDisplay: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var viewport: DDRUMLongTaskEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMLongTaskEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMLongTaskEventDisplayViewport: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMLongTaskEventLongTask: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + 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 + } + + @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 +public class DDRUMLongTaskEventRUMOperatingSystem: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMLongTaskEventSession: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var type: DDRUMLongTaskEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMLongTaskEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMLongTaskEventSource: Int { + internal init(swift: RUMLongTaskEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMLongTaskEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMLongTaskEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMLongTaskEventRUMUser: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMLongTaskEventView: NSObject { + internal let root: DDRUMLongTaskEvent + + internal init(root: DDRUMLongTaskEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + + @objc public var referrer: String? { + set { root.swiftModel.view.referrer = newValue } + get { root.swiftModel.view.referrer } + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMResourceEvent: NSObject { + internal var swiftModel: RUMResourceEvent + internal var root: DDRUMResourceEvent { self } + + internal init(swiftModel: RUMResourceEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMResourceEventDD { + DDRUMResourceEventDD(root: root) + } + + @objc public var action: DDRUMResourceEventAction? { + root.swiftModel.action != nil ? DDRUMResourceEventAction(root: root) : nil + } + + @objc public var application: DDRUMResourceEventApplication { + DDRUMResourceEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMResourceEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMResourceEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMResourceEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMResourceEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMResourceEventContainer? { + root.swiftModel.container != nil ? DDRUMResourceEventContainer(root: root) : nil + } + + @objc public var context: DDRUMResourceEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMResourceEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMResourceEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMResourceEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMResourceEventDisplay? { + root.swiftModel.display != nil ? DDRUMResourceEventDisplay(root: root) : nil + } + + @objc public var os: DDRUMResourceEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMResourceEventRUMOperatingSystem(root: root) : nil + } + + @objc public var resource: DDRUMResourceEventResource { + DDRUMResourceEventResource(root: root) + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMResourceEventSession { + DDRUMResourceEventSession(root: root) + } + + @objc public var source: DDRUMResourceEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMResourceEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMResourceEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMResourceEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMResourceEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMResourceEventView { + DDRUMResourceEventView(root: root) + } +} + +@objc +public class DDRUMResourceEventDD: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMResourceEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMResourceEventDDConfiguration(root: root) : nil + } + + @objc public var discarded: NSNumber? { + root.swiftModel.dd.discarded as NSNumber? + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var rulePsr: NSNumber? { + root.swiftModel.dd.rulePsr as NSNumber? + } + + @objc public var session: DDRUMResourceEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMResourceEventDDSession(root: root) : nil + } + + @objc public var spanId: String? { + root.swiftModel.dd.spanId + } + + @objc public var traceId: String? { + root.swiftModel.dd.traceId + } +} + +@objc +public class DDRUMResourceEventDDConfiguration: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } +} + +@objc +public class DDRUMResourceEventDDSession: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var plan: DDRUMResourceEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMResourceEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMResourceEventDDSessionPlan: Int { + internal init(swift: RUMResourceEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMResourceEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMResourceEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMResourceEventAction: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var id: DDRUMResourceEventActionRUMActionID { + DDRUMResourceEventActionRUMActionID(root: root) + } +} + +@objc +public class DDRUMResourceEventActionRUMActionID: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var string: String? { + guard case .string(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } + + @objc public var stringsArray: [String]? { + guard case .stringsArray(let value) = root.swiftModel.action!.id else { + return nil + } + return value + } +} + +@objc +public class DDRUMResourceEventApplication: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMResourceEventRUMCITest: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMResourceEventRUMConnectivity: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var cellular: DDRUMResourceEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMResourceEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMResourceEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMResourceEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMResourceEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMResourceEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMResourceEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMResourceEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMResourceEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMResourceEventContainer: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var source: DDRUMResourceEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMResourceEventContainerView { + DDRUMResourceEventContainerView(root: root) + } +} + +@objc +public enum DDRUMResourceEventContainerSource: Int { + internal init(swift: RUMResourceEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMResourceEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMResourceEventContainerView: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMResourceEventRUMEventAttributes: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMResourceEventRUMDevice: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMResourceEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMResourceEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMResourceEventDisplay: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var viewport: DDRUMResourceEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMResourceEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMResourceEventDisplayViewport: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMResourceEventRUMOperatingSystem: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMResourceEventResource: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var connect: DDRUMResourceEventResourceConnect? { + root.swiftModel.resource.connect != nil ? DDRUMResourceEventResourceConnect(root: root) : nil + } + + @objc public var decodedBodySize: NSNumber? { + 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 + } + + @objc public var download: DDRUMResourceEventResourceDownload? { + root.swiftModel.resource.download != nil ? DDRUMResourceEventResourceDownload(root: root) : nil + } + + @objc public var duration: NSNumber? { + root.swiftModel.resource.duration as NSNumber? + } + + @objc public var encodedBodySize: NSNumber? { + root.swiftModel.resource.encodedBodySize as NSNumber? + } + + @objc public var firstByte: DDRUMResourceEventResourceFirstByte? { + root.swiftModel.resource.firstByte != nil ? DDRUMResourceEventResourceFirstByte(root: root) : nil + } + + @objc public var graphql: DDRUMResourceEventResourceGraphql? { + root.swiftModel.resource.graphql != nil ? DDRUMResourceEventResourceGraphql(root: root) : nil + } + + @objc public var id: String? { + root.swiftModel.resource.id + } + + @objc public var method: DDRUMResourceEventResourceRUMMethod { + .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 + } + + @objc public var redirect: DDRUMResourceEventResourceRedirect? { + root.swiftModel.resource.redirect != nil ? DDRUMResourceEventResourceRedirect(root: root) : nil + } + + @objc public var renderBlockingStatus: DDRUMResourceEventResourceRenderBlockingStatus { + .init(swift: root.swiftModel.resource.renderBlockingStatus) + } + + @objc public var size: NSNumber? { + root.swiftModel.resource.size as NSNumber? + } + + @objc public var ssl: DDRUMResourceEventResourceSSL? { + root.swiftModel.resource.ssl != nil ? DDRUMResourceEventResourceSSL(root: root) : nil + } + + @objc public var statusCode: NSNumber? { + root.swiftModel.resource.statusCode as NSNumber? + } + + @objc public var transferSize: NSNumber? { + root.swiftModel.resource.transferSize as NSNumber? + } + + @objc public var type: DDRUMResourceEventResourceResourceType { + .init(swift: root.swiftModel.resource.type) + } + + @objc public var url: String { + 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 +public class DDRUMResourceEventResourceConnect: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.connect!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.connect!.start as NSNumber + } +} + +@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 + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.dns!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.dns!.start as NSNumber + } +} + +@objc +public class DDRUMResourceEventResourceDownload: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.download!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.download!.start as NSNumber + } +} + +@objc +public class DDRUMResourceEventResourceFirstByte: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.firstByte!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.firstByte!.start as NSNumber + } +} + +@objc +public class DDRUMResourceEventResourceGraphql: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var operationName: String? { + root.swiftModel.resource.graphql!.operationName + } + + @objc public var operationType: DDRUMResourceEventResourceGraphqlOperationType { + .init(swift: root.swiftModel.resource.graphql!.operationType) + } + + @objc public var payload: String? { + set { root.swiftModel.resource.graphql!.payload = newValue } + get { root.swiftModel.resource.graphql!.payload } + } + + @objc public var variables: String? { + set { root.swiftModel.resource.graphql!.variables = newValue } + get { root.swiftModel.resource.graphql!.variables } + } +} + +@objc +public enum DDRUMResourceEventResourceGraphqlOperationType: Int { + internal init(swift: RUMResourceEvent.Resource.Graphql.OperationType) { + switch swift { + case .query: self = .query + case .mutation: self = .mutation + case .subscription: self = .subscription + } + } + + internal var toSwift: RUMResourceEvent.Resource.Graphql.OperationType { + switch self { + case .query: return .query + case .mutation: return .mutation + case .subscription: return .subscription + } + } + + case query + case mutation + case subscription +} + +@objc +public enum DDRUMResourceEventResourceRUMMethod: Int { + internal init(swift: RUMMethod?) { + switch swift { + case nil: self = .none + case .post?: self = .post + case .get?: self = .get + case .head?: self = .head + case .put?: self = .put + case .delete?: self = .delete + case .patch?: self = .patch + case .trace?: self = .trace + case .options?: self = .options + case .connect?: self = .connect + } + } + + internal var toSwift: RUMMethod? { + switch self { + case .none: return nil + case .post: return .post + case .get: return .get + case .head: return .head + case .put: return .put + case .delete: return .delete + case .patch: return .patch + case .trace: return .trace + case .options: return .options + case .connect: return .connect + } + } + + case none + case post + case get + case head + case put + case delete + case patch + case trace + case options + case connect +} + +@objc +public class DDRUMResourceEventResourceProvider: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var domain: String? { + root.swiftModel.resource.provider!.domain + } + + @objc public var name: String? { + root.swiftModel.resource.provider!.name + } + + @objc public var type: DDRUMResourceEventResourceProviderProviderType { + .init(swift: root.swiftModel.resource.provider!.type) + } +} + +@objc +public enum DDRUMResourceEventResourceProviderProviderType: Int { + internal init(swift: RUMResourceEvent.Resource.Provider.ProviderType?) { + switch swift { + case nil: self = .none + case .ad?: self = .ad + case .advertising?: self = .advertising + case .analytics?: self = .analytics + case .cdn?: self = .cdn + case .content?: self = .content + case .customerSuccess?: self = .customerSuccess + case .firstParty?: self = .firstParty + case .hosting?: self = .hosting + case .marketing?: self = .marketing + case .other?: self = .other + case .social?: self = .social + case .tagManager?: self = .tagManager + case .utility?: self = .utility + case .video?: self = .video + } + } + + internal var toSwift: RUMResourceEvent.Resource.Provider.ProviderType? { + switch self { + case .none: return nil + case .ad: return .ad + case .advertising: return .advertising + case .analytics: return .analytics + case .cdn: return .cdn + case .content: return .content + case .customerSuccess: return .customerSuccess + case .firstParty: return .firstParty + case .hosting: return .hosting + case .marketing: return .marketing + case .other: return .other + case .social: return .social + case .tagManager: return .tagManager + case .utility: return .utility + case .video: return .video + } + } + + case none + case ad + case advertising + case analytics + case cdn + case content + case customerSuccess + case firstParty + case hosting + case marketing + case other + case social + case tagManager + case utility + case video +} + +@objc +public class DDRUMResourceEventResourceRedirect: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.redirect!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.redirect!.start as NSNumber + } +} + +@objc +public enum DDRUMResourceEventResourceRenderBlockingStatus: Int { + internal init(swift: RUMResourceEvent.Resource.RenderBlockingStatus?) { + switch swift { + case nil: self = .none + case .blocking?: self = .blocking + case .nonBlocking?: self = .nonBlocking + } + } + + internal var toSwift: RUMResourceEvent.Resource.RenderBlockingStatus? { + switch self { + case .none: return nil + case .blocking: return .blocking + case .nonBlocking: return .nonBlocking + } + } + + case none + case blocking + case nonBlocking +} + +@objc +public class DDRUMResourceEventResourceSSL: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var duration: NSNumber { + root.swiftModel.resource.ssl!.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.resource.ssl!.start as NSNumber + } +} + +@objc +public enum DDRUMResourceEventResourceResourceType: Int { + internal init(swift: RUMResourceEvent.Resource.ResourceType) { + switch swift { + case .document: self = .document + case .xhr: self = .xhr + case .beacon: self = .beacon + case .fetch: self = .fetch + case .css: self = .css + case .js: self = .js + case .image: self = .image + case .font: self = .font + case .media: self = .media + case .other: self = .other + case .native: self = .native + } + } + + internal var toSwift: RUMResourceEvent.Resource.ResourceType { + switch self { + case .document: return .document + case .xhr: return .xhr + case .beacon: return .beacon + case .fetch: return .fetch + case .css: return .css + case .js: return .js + case .image: return .image + case .font: return .font + case .media: return .media + case .other: return .other + case .native: return .native + } + } + + case document + case xhr + case beacon + case fetch + case css + case js + case image + case font + case media + case other + 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 + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var type: DDRUMResourceEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMResourceEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMResourceEventSource: Int { + internal init(swift: RUMResourceEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMResourceEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMResourceEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMResourceEventRUMUser: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMResourceEventView: NSObject { + internal let root: DDRUMResourceEvent + + internal init(root: DDRUMResourceEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + + @objc public var referrer: String? { + set { root.swiftModel.view.referrer = newValue } + get { root.swiftModel.view.referrer } + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMViewEvent: NSObject { + internal var swiftModel: RUMViewEvent + internal var root: DDRUMViewEvent { self } + + internal init(swiftModel: RUMViewEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMViewEventDD { + DDRUMViewEventDD(root: root) + } + + @objc public var application: DDRUMViewEventApplication { + DDRUMViewEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMViewEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMViewEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMViewEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMViewEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMViewEventContainer? { + root.swiftModel.container != nil ? DDRUMViewEventContainer(root: root) : nil + } + + @objc public var context: DDRUMViewEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMViewEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMViewEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMViewEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMViewEventDisplay? { + root.swiftModel.display != nil ? DDRUMViewEventDisplay(root: root) : nil + } + + @objc public var featureFlags: DDRUMViewEventFeatureFlags? { + root.swiftModel.featureFlags != nil ? DDRUMViewEventFeatureFlags(root: root) : nil + } + + @objc public var os: DDRUMViewEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMViewEventRUMOperatingSystem(root: root) : nil + } + + @objc public var privacy: DDRUMViewEventPrivacy? { + root.swiftModel.privacy != nil ? DDRUMViewEventPrivacy(root: root) : nil + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMViewEventSession { + DDRUMViewEventSession(root: root) + } + + @objc public var source: DDRUMViewEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMViewEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMViewEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMViewEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMViewEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMViewEventView { + DDRUMViewEventView(root: root) + } +} + +@objc +public class DDRUMViewEventDD: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMViewEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMViewEventDDConfiguration(root: root) : nil + } + + @objc public var documentVersion: NSNumber { + root.swiftModel.dd.documentVersion as NSNumber + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var pageStates: [DDRUMViewEventDDPageStates]? { + root.swiftModel.dd.pageStates?.map { DDRUMViewEventDDPageStates(swiftModel: $0) } + } + + @objc public var replayStats: DDRUMViewEventDDReplayStats? { + root.swiftModel.dd.replayStats != nil ? DDRUMViewEventDDReplayStats(root: root) : nil + } + + @objc public var session: DDRUMViewEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMViewEventDDSession(root: root) : nil + } +} + +@objc +public class DDRUMViewEventDDConfiguration: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } + + @objc public var startSessionReplayRecordingManually: NSNumber? { + root.swiftModel.dd.configuration!.startSessionReplayRecordingManually as NSNumber? + } +} + +@objc +public class DDRUMViewEventDDPageStates: NSObject { + internal var swiftModel: RUMViewEvent.DD.PageStates + internal var root: DDRUMViewEventDDPageStates { self } + + internal init(swiftModel: RUMViewEvent.DD.PageStates) { + self.swiftModel = swiftModel + } + + @objc public var start: NSNumber { + root.swiftModel.start as NSNumber + } + + @objc public var state: DDRUMViewEventDDPageStatesState { + .init(swift: root.swiftModel.state) + } +} + +@objc +public enum DDRUMViewEventDDPageStatesState: Int { + internal init(swift: RUMViewEvent.DD.PageStates.State) { + switch swift { + case .active: self = .active + case .passive: self = .passive + case .hidden: self = .hidden + case .frozen: self = .frozen + case .terminated: self = .terminated + } + } + + internal var toSwift: RUMViewEvent.DD.PageStates.State { + switch self { + case .active: return .active + case .passive: return .passive + case .hidden: return .hidden + case .frozen: return .frozen + case .terminated: return .terminated + } + } + + case active + case passive + case hidden + case frozen + case terminated +} + +@objc +public class DDRUMViewEventDDReplayStats: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var recordsCount: NSNumber? { + root.swiftModel.dd.replayStats!.recordsCount as NSNumber? + } + + @objc public var segmentsCount: NSNumber? { + root.swiftModel.dd.replayStats!.segmentsCount as NSNumber? + } + + @objc public var segmentsTotalRawSize: NSNumber? { + root.swiftModel.dd.replayStats!.segmentsTotalRawSize as NSNumber? + } +} + +@objc +public class DDRUMViewEventDDSession: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var plan: DDRUMViewEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMViewEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMViewEventDDSessionPlan: Int { + internal init(swift: RUMViewEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMViewEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMViewEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMViewEventApplication: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMViewEventRUMCITest: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMViewEventRUMConnectivity: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var cellular: DDRUMViewEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMViewEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMViewEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMViewEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMViewEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMViewEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMViewEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMViewEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMViewEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMViewEventContainer: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var source: DDRUMViewEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMViewEventContainerView { + DDRUMViewEventContainerView(root: root) + } +} + +@objc +public enum DDRUMViewEventContainerSource: Int { + internal init(swift: RUMViewEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMViewEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMViewEventContainerView: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMViewEventRUMEventAttributes: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMViewEventRUMDevice: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMViewEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMViewEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMViewEventDisplay: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var scroll: DDRUMViewEventDisplayScroll? { + root.swiftModel.display!.scroll != nil ? DDRUMViewEventDisplayScroll(root: root) : nil + } + + @objc public var viewport: DDRUMViewEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMViewEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMViewEventDisplayScroll: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var maxDepth: NSNumber { + root.swiftModel.display!.scroll!.maxDepth as NSNumber + } + + @objc public var maxDepthScrollTop: NSNumber { + root.swiftModel.display!.scroll!.maxDepthScrollTop as NSNumber + } + + @objc public var maxScrollHeight: NSNumber { + root.swiftModel.display!.scroll!.maxScrollHeight as NSNumber + } + + @objc public var maxScrollHeightTime: NSNumber { + root.swiftModel.display!.scroll!.maxScrollHeightTime as NSNumber + } +} + +@objc +public class DDRUMViewEventDisplayViewport: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMViewEventFeatureFlags: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var featureFlagsInfo: [String: Any] { + set { root.swiftModel.featureFlags!.featureFlagsInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.featureFlags!.featureFlagsInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMViewEventRUMOperatingSystem: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMViewEventPrivacy: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var replayLevel: DDRUMViewEventPrivacyReplayLevel { + .init(swift: root.swiftModel.privacy!.replayLevel) + } +} + +@objc +public enum DDRUMViewEventPrivacyReplayLevel: Int { + internal init(swift: RUMViewEvent.Privacy.ReplayLevel) { + switch swift { + case .allow: self = .allow + case .mask: self = .mask + case .maskUserInput: self = .maskUserInput + } + } + + internal var toSwift: RUMViewEvent.Privacy.ReplayLevel { + switch self { + case .allow: return .allow + case .mask: return .mask + case .maskUserInput: return .maskUserInput + } + } + + case allow + case mask + case maskUserInput +} + +@objc +public class DDRUMViewEventSession: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var isActive: NSNumber? { + root.swiftModel.session.isActive as NSNumber? + } + + @objc public var sampledForReplay: NSNumber? { + root.swiftModel.session.sampledForReplay as NSNumber? + } + + @objc public var type: DDRUMViewEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMViewEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMViewEventSource: Int { + internal init(swift: RUMViewEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMViewEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMViewEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMViewEventRUMUser: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMViewEventView: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var action: DDRUMViewEventViewAction { + DDRUMViewEventViewAction(root: root) + } + + @objc public var cpuTicksCount: NSNumber? { + root.swiftModel.view.cpuTicksCount as NSNumber? + } + + @objc public var cpuTicksPerSecond: NSNumber? { + root.swiftModel.view.cpuTicksPerSecond as NSNumber? + } + + @objc public var crash: DDRUMViewEventViewCrash? { + root.swiftModel.view.crash != nil ? DDRUMViewEventViewCrash(root: root) : nil + } + + @objc public var cumulativeLayoutShift: NSNumber? { + root.swiftModel.view.cumulativeLayoutShift as NSNumber? + } + + @objc public var cumulativeLayoutShiftTargetSelector: String? { + root.swiftModel.view.cumulativeLayoutShiftTargetSelector + } + + @objc public var cumulativeLayoutShiftTime: NSNumber? { + root.swiftModel.view.cumulativeLayoutShiftTime as NSNumber? + } + + @objc public var customTimings: [String: NSNumber]? { + root.swiftModel.view.customTimings as [String: NSNumber]? + } + + @objc public var domComplete: NSNumber? { + root.swiftModel.view.domComplete as NSNumber? + } + + @objc public var domContentLoaded: NSNumber? { + root.swiftModel.view.domContentLoaded as NSNumber? + } + + @objc public var domInteractive: NSNumber? { + root.swiftModel.view.domInteractive as NSNumber? + } + + @objc public var error: DDRUMViewEventViewError { + DDRUMViewEventViewError(root: root) + } + + @objc public var firstByte: NSNumber? { + root.swiftModel.view.firstByte as NSNumber? + } + + @objc public var firstContentfulPaint: NSNumber? { + root.swiftModel.view.firstContentfulPaint as NSNumber? + } + + @objc public var firstInputDelay: NSNumber? { + root.swiftModel.view.firstInputDelay as NSNumber? + } + + @objc public var firstInputTargetSelector: String? { + root.swiftModel.view.firstInputTargetSelector + } + + @objc public var firstInputTime: NSNumber? { + root.swiftModel.view.firstInputTime as NSNumber? + } + + @objc public var flutterBuildTime: DDRUMViewEventViewFlutterBuildTime? { + root.swiftModel.view.flutterBuildTime != nil ? DDRUMViewEventViewFlutterBuildTime(root: root) : nil + } + + @objc public var flutterRasterTime: DDRUMViewEventViewFlutterRasterTime? { + root.swiftModel.view.flutterRasterTime != nil ? DDRUMViewEventViewFlutterRasterTime(root: root) : nil + } + + @objc public var frozenFrame: DDRUMViewEventViewFrozenFrame? { + root.swiftModel.view.frozenFrame != nil ? DDRUMViewEventViewFrozenFrame(root: root) : nil + } + + @objc public var frustration: DDRUMViewEventViewFrustration? { + root.swiftModel.view.frustration != nil ? DDRUMViewEventViewFrustration(root: root) : nil + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var inForegroundPeriods: [DDRUMViewEventViewInForegroundPeriods]? { + root.swiftModel.view.inForegroundPeriods?.map { DDRUMViewEventViewInForegroundPeriods(swiftModel: $0) } + } + + @objc public var interactionToNextPaint: NSNumber? { + root.swiftModel.view.interactionToNextPaint as NSNumber? + } + + @objc public var interactionToNextPaintTargetSelector: String? { + root.swiftModel.view.interactionToNextPaintTargetSelector + } + + @objc public var interactionToNextPaintTime: NSNumber? { + 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? + } + + @objc public var isSlowRendered: NSNumber? { + root.swiftModel.view.isSlowRendered as NSNumber? + } + + @objc public var jsRefreshRate: DDRUMViewEventViewJsRefreshRate? { + root.swiftModel.view.jsRefreshRate != nil ? DDRUMViewEventViewJsRefreshRate(root: root) : nil + } + + @objc public var largestContentfulPaint: NSNumber? { + root.swiftModel.view.largestContentfulPaint as NSNumber? + } + + @objc public var largestContentfulPaintTargetSelector: String? { + root.swiftModel.view.largestContentfulPaintTargetSelector + } + + @objc public var loadEvent: NSNumber? { + root.swiftModel.view.loadEvent as NSNumber? + } + + @objc public var loadingTime: NSNumber? { + root.swiftModel.view.loadingTime as NSNumber? + } + + @objc public var loadingType: DDRUMViewEventViewLoadingType { + .init(swift: root.swiftModel.view.loadingType) + } + + @objc public var longTask: DDRUMViewEventViewLongTask? { + root.swiftModel.view.longTask != nil ? DDRUMViewEventViewLongTask(root: root) : nil + } + + @objc public var memoryAverage: NSNumber? { + root.swiftModel.view.memoryAverage as NSNumber? + } + + @objc public var memoryMax: NSNumber? { + root.swiftModel.view.memoryMax as NSNumber? + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + 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 } + } + + @objc public var refreshRateAverage: NSNumber? { + root.swiftModel.view.refreshRateAverage as NSNumber? + } + + @objc public var refreshRateMin: NSNumber? { + root.swiftModel.view.refreshRateMin as NSNumber? + } + + @objc public var resource: DDRUMViewEventViewResource { + DDRUMViewEventViewResource(root: root) + } + + @objc public var timeSpent: NSNumber { + root.swiftModel.view.timeSpent as NSNumber + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMViewEventViewAction: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.action.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewCrash: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.crash!.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewError: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.error.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewFlutterBuildTime: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var average: NSNumber { + root.swiftModel.view.flutterBuildTime!.average as NSNumber + } + + @objc public var max: NSNumber { + root.swiftModel.view.flutterBuildTime!.max as NSNumber + } + + @objc public var metricMax: NSNumber? { + root.swiftModel.view.flutterBuildTime!.metricMax as NSNumber? + } + + @objc public var min: NSNumber { + root.swiftModel.view.flutterBuildTime!.min as NSNumber + } +} + +@objc +public class DDRUMViewEventViewFlutterRasterTime: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var average: NSNumber { + root.swiftModel.view.flutterRasterTime!.average as NSNumber + } + + @objc public var max: NSNumber { + root.swiftModel.view.flutterRasterTime!.max as NSNumber + } + + @objc public var metricMax: NSNumber? { + root.swiftModel.view.flutterRasterTime!.metricMax as NSNumber? + } + + @objc public var min: NSNumber { + root.swiftModel.view.flutterRasterTime!.min as NSNumber + } +} + +@objc +public class DDRUMViewEventViewFrozenFrame: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.frozenFrame!.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewFrustration: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.frustration!.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewInForegroundPeriods: NSObject { + internal var swiftModel: RUMViewEvent.View.InForegroundPeriods + internal var root: DDRUMViewEventViewInForegroundPeriods { self } + + internal init(swiftModel: RUMViewEvent.View.InForegroundPeriods) { + self.swiftModel = swiftModel + } + + @objc public var duration: NSNumber { + root.swiftModel.duration as NSNumber + } + + @objc public var start: NSNumber { + root.swiftModel.start as NSNumber + } +} + +@objc +public class DDRUMViewEventViewJsRefreshRate: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var average: NSNumber { + root.swiftModel.view.jsRefreshRate!.average as NSNumber + } + + @objc public var max: NSNumber { + root.swiftModel.view.jsRefreshRate!.max as NSNumber + } + + @objc public var metricMax: NSNumber? { + root.swiftModel.view.jsRefreshRate!.metricMax as NSNumber? + } + + @objc public var min: NSNumber { + root.swiftModel.view.jsRefreshRate!.min as NSNumber + } +} + +@objc +public enum DDRUMViewEventViewLoadingType: Int { + internal init(swift: RUMViewEvent.View.LoadingType?) { + switch swift { + case nil: self = .none + case .initialLoad?: self = .initialLoad + case .routeChange?: self = .routeChange + case .activityDisplay?: self = .activityDisplay + case .activityRedisplay?: self = .activityRedisplay + case .fragmentDisplay?: self = .fragmentDisplay + case .fragmentRedisplay?: self = .fragmentRedisplay + case .viewControllerDisplay?: self = .viewControllerDisplay + case .viewControllerRedisplay?: self = .viewControllerRedisplay + } + } + + internal var toSwift: RUMViewEvent.View.LoadingType? { + switch self { + case .none: return nil + case .initialLoad: return .initialLoad + case .routeChange: return .routeChange + case .activityDisplay: return .activityDisplay + case .activityRedisplay: return .activityRedisplay + case .fragmentDisplay: return .fragmentDisplay + case .fragmentRedisplay: return .fragmentRedisplay + case .viewControllerDisplay: return .viewControllerDisplay + case .viewControllerRedisplay: return .viewControllerRedisplay + } + } + + case none + case initialLoad + case routeChange + case activityDisplay + case activityRedisplay + case fragmentDisplay + case fragmentRedisplay + case viewControllerDisplay + case viewControllerRedisplay +} + +@objc +public class DDRUMViewEventViewLongTask: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.longTask!.count as NSNumber + } +} + +@objc +public class DDRUMViewEventViewResource: NSObject { + internal let root: DDRUMViewEvent + + internal init(root: DDRUMViewEvent) { + self.root = root + } + + @objc public var count: NSNumber { + root.swiftModel.view.resource.count as NSNumber + } +} + +@objc +public class DDRUMVitalEvent: NSObject { + internal var swiftModel: RUMVitalEvent + internal var root: DDRUMVitalEvent { self } + + internal init(swiftModel: RUMVitalEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDRUMVitalEventDD { + DDRUMVitalEventDD(root: root) + } + + @objc public var application: DDRUMVitalEventApplication { + DDRUMVitalEventApplication(root: root) + } + + @objc public var buildId: String? { + root.swiftModel.buildId + } + + @objc public var buildVersion: String? { + root.swiftModel.buildVersion + } + + @objc public var ciTest: DDRUMVitalEventRUMCITest? { + root.swiftModel.ciTest != nil ? DDRUMVitalEventRUMCITest(root: root) : nil + } + + @objc public var connectivity: DDRUMVitalEventRUMConnectivity? { + root.swiftModel.connectivity != nil ? DDRUMVitalEventRUMConnectivity(root: root) : nil + } + + @objc public var container: DDRUMVitalEventContainer? { + root.swiftModel.container != nil ? DDRUMVitalEventContainer(root: root) : nil + } + + @objc public var context: DDRUMVitalEventRUMEventAttributes? { + root.swiftModel.context != nil ? DDRUMVitalEventRUMEventAttributes(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var device: DDRUMVitalEventRUMDevice? { + root.swiftModel.device != nil ? DDRUMVitalEventRUMDevice(root: root) : nil + } + + @objc public var display: DDRUMVitalEventDisplay? { + root.swiftModel.display != nil ? DDRUMVitalEventDisplay(root: root) : nil + } + + @objc public var os: DDRUMVitalEventRUMOperatingSystem? { + root.swiftModel.os != nil ? DDRUMVitalEventRUMOperatingSystem(root: root) : nil + } + + @objc public var service: String? { + root.swiftModel.service + } + + @objc public var session: DDRUMVitalEventSession { + DDRUMVitalEventSession(root: root) + } + + @objc public var source: DDRUMVitalEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var synthetics: DDRUMVitalEventRUMSyntheticsTest? { + root.swiftModel.synthetics != nil ? DDRUMVitalEventRUMSyntheticsTest(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var usr: DDRUMVitalEventRUMUser? { + root.swiftModel.usr != nil ? DDRUMVitalEventRUMUser(root: root) : nil + } + + @objc public var version: String? { + root.swiftModel.version + } + + @objc public var view: DDRUMVitalEventView { + DDRUMVitalEventView(root: root) + } + + @objc public var vital: DDRUMVitalEventVital { + DDRUMVitalEventVital(root: root) + } +} + +@objc +public class DDRUMVitalEventDD: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var browserSdkVersion: String? { + root.swiftModel.dd.browserSdkVersion + } + + @objc public var configuration: DDRUMVitalEventDDConfiguration? { + root.swiftModel.dd.configuration != nil ? DDRUMVitalEventDDConfiguration(root: root) : nil + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } + + @objc public var session: DDRUMVitalEventDDSession? { + root.swiftModel.dd.session != nil ? DDRUMVitalEventDDSession(root: root) : nil + } + + @objc public var vital: DDRUMVitalEventDDVital? { + root.swiftModel.dd.vital != nil ? DDRUMVitalEventDDVital(root: root) : nil + } +} + +@objc +public class DDRUMVitalEventDDConfiguration: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var sessionReplaySampleRate: NSNumber? { + root.swiftModel.dd.configuration!.sessionReplaySampleRate as NSNumber? + } + + @objc public var sessionSampleRate: NSNumber { + root.swiftModel.dd.configuration!.sessionSampleRate as NSNumber + } +} + +@objc +public class DDRUMVitalEventDDSession: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var plan: DDRUMVitalEventDDSessionPlan { + .init(swift: root.swiftModel.dd.session!.plan) + } + + @objc public var sessionPrecondition: DDRUMVitalEventDDSessionRUMSessionPrecondition { + .init(swift: root.swiftModel.dd.session!.sessionPrecondition) + } +} + +@objc +public enum DDRUMVitalEventDDSessionPlan: Int { + internal init(swift: RUMVitalEvent.DD.Session.Plan?) { + switch swift { + case nil: self = .none + case .plan1?: self = .plan1 + case .plan2?: self = .plan2 + } + } + + internal var toSwift: RUMVitalEvent.DD.Session.Plan? { + switch self { + case .none: return nil + case .plan1: return .plan1 + case .plan2: return .plan2 + } + } + + case none + case plan1 + case plan2 +} + +@objc +public enum DDRUMVitalEventDDSessionRUMSessionPrecondition: Int { + internal init(swift: RUMSessionPrecondition?) { + switch swift { + case nil: self = .none + case .userAppLaunch?: self = .userAppLaunch + case .inactivityTimeout?: self = .inactivityTimeout + case .maxDuration?: self = .maxDuration + case .backgroundLaunch?: self = .backgroundLaunch + case .prewarm?: self = .prewarm + case .fromNonInteractiveSession?: self = .fromNonInteractiveSession + case .explicitStop?: self = .explicitStop + } + } + + internal var toSwift: RUMSessionPrecondition? { + switch self { + case .none: return nil + case .userAppLaunch: return .userAppLaunch + case .inactivityTimeout: return .inactivityTimeout + case .maxDuration: return .maxDuration + case .backgroundLaunch: return .backgroundLaunch + case .prewarm: return .prewarm + case .fromNonInteractiveSession: return .fromNonInteractiveSession + case .explicitStop: return .explicitStop + } + } + + case none + case userAppLaunch + case inactivityTimeout + case maxDuration + case backgroundLaunch + case prewarm + case fromNonInteractiveSession + case explicitStop +} + +@objc +public class DDRUMVitalEventDDVital: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var computedValue: NSNumber? { + root.swiftModel.dd.vital!.computedValue as NSNumber? + } +} + +@objc +public class DDRUMVitalEventApplication: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application.id + } +} + +@objc +public class DDRUMVitalEventRUMCITest: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var testExecutionId: String { + root.swiftModel.ciTest!.testExecutionId + } +} + +@objc +public class DDRUMVitalEventRUMConnectivity: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var cellular: DDRUMVitalEventRUMConnectivityCellular? { + root.swiftModel.connectivity!.cellular != nil ? DDRUMVitalEventRUMConnectivityCellular(root: root) : nil + } + + @objc public var effectiveType: DDRUMVitalEventRUMConnectivityEffectiveType { + .init(swift: root.swiftModel.connectivity!.effectiveType) + } + + @objc public var interfaces: [Int]? { + root.swiftModel.connectivity!.interfaces?.map { DDRUMVitalEventRUMConnectivityInterfaces(swift: $0).rawValue } + } + + @objc public var status: DDRUMVitalEventRUMConnectivityStatus { + .init(swift: root.swiftModel.connectivity!.status) + } +} + +@objc +public class DDRUMVitalEventRUMConnectivityCellular: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var carrierName: String? { + root.swiftModel.connectivity!.cellular!.carrierName + } + + @objc public var technology: String? { + root.swiftModel.connectivity!.cellular!.technology + } +} + +@objc +public enum DDRUMVitalEventRUMConnectivityEffectiveType: Int { + internal init(swift: RUMConnectivity.EffectiveType?) { + switch swift { + case nil: self = .none + case .slow2g?: self = .slow2g + case .effectiveType2g?: self = .effectiveType2g + case .effectiveType3g?: self = .effectiveType3g + case .effectiveType4g?: self = .effectiveType4g + } + } + + internal var toSwift: RUMConnectivity.EffectiveType? { + switch self { + case .none: return nil + case .slow2g: return .slow2g + case .effectiveType2g: return .effectiveType2g + case .effectiveType3g: return .effectiveType3g + case .effectiveType4g: return .effectiveType4g + } + } + + case none + case slow2g + case effectiveType2g + case effectiveType3g + case effectiveType4g +} + +@objc +public enum DDRUMVitalEventRUMConnectivityInterfaces: Int { + internal init(swift: RUMConnectivity.Interfaces?) { + switch swift { + case nil: self = .none + case .bluetooth?: self = .bluetooth + case .cellular?: self = .cellular + case .ethernet?: self = .ethernet + case .wifi?: self = .wifi + case .wimax?: self = .wimax + case .mixed?: self = .mixed + case .other?: self = .other + case .unknown?: self = .unknown + case .interfacesNone?: self = .interfacesNone + } + } + + internal var toSwift: RUMConnectivity.Interfaces? { + switch self { + case .none: return nil + case .bluetooth: return .bluetooth + case .cellular: return .cellular + case .ethernet: return .ethernet + case .wifi: return .wifi + case .wimax: return .wimax + case .mixed: return .mixed + case .other: return .other + case .unknown: return .unknown + case .interfacesNone: return .interfacesNone + } + } + + case none + case bluetooth + case cellular + case ethernet + case wifi + case wimax + case mixed + case other + case unknown + case interfacesNone +} + +@objc +public enum DDRUMVitalEventRUMConnectivityStatus: Int { + internal init(swift: RUMConnectivity.Status) { + switch swift { + case .connected: self = .connected + case .notConnected: self = .notConnected + case .maybe: self = .maybe + } + } + + internal var toSwift: RUMConnectivity.Status { + switch self { + case .connected: return .connected + case .notConnected: return .notConnected + case .maybe: return .maybe + } + } + + case connected + case notConnected + case maybe +} + +@objc +public class DDRUMVitalEventContainer: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var source: DDRUMVitalEventContainerSource { + .init(swift: root.swiftModel.container!.source) + } + + @objc public var view: DDRUMVitalEventContainerView { + DDRUMVitalEventContainerView(root: root) + } +} + +@objc +public enum DDRUMVitalEventContainerSource: Int { + internal init(swift: RUMVitalEvent.Container.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .roku: self = .roku + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMVitalEvent.Container.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMVitalEventContainerView: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.container!.view.id + } +} + +@objc +public class DDRUMVitalEventRUMEventAttributes: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var contextInfo: [String: Any] { + set { root.swiftModel.context!.contextInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.context!.contextInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMVitalEventRUMDevice: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.device!.brand + } + + @objc public var model: String? { + root.swiftModel.device!.model + } + + @objc public var name: String? { + root.swiftModel.device!.name + } + + @objc public var type: DDRUMVitalEventRUMDeviceRUMDeviceType { + .init(swift: root.swiftModel.device!.type) + } +} + +@objc +public enum DDRUMVitalEventRUMDeviceRUMDeviceType: Int { + internal init(swift: RUMDevice.RUMDeviceType) { + switch swift { + case .mobile: self = .mobile + case .desktop: self = .desktop + case .tablet: self = .tablet + case .tv: self = .tv + case .gamingConsole: self = .gamingConsole + case .bot: self = .bot + case .other: self = .other + } + } + + internal var toSwift: RUMDevice.RUMDeviceType { + switch self { + case .mobile: return .mobile + case .desktop: return .desktop + case .tablet: return .tablet + case .tv: return .tv + case .gamingConsole: return .gamingConsole + case .bot: return .bot + case .other: return .other + } + } + + case mobile + case desktop + case tablet + case tv + case gamingConsole + case bot + case other +} + +@objc +public class DDRUMVitalEventDisplay: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var viewport: DDRUMVitalEventDisplayViewport? { + root.swiftModel.display!.viewport != nil ? DDRUMVitalEventDisplayViewport(root: root) : nil + } +} + +@objc +public class DDRUMVitalEventDisplayViewport: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var height: NSNumber { + root.swiftModel.display!.viewport!.height as NSNumber + } + + @objc public var width: NSNumber { + root.swiftModel.display!.viewport!.width as NSNumber + } +} + +@objc +public class DDRUMVitalEventRUMOperatingSystem: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.os!.build + } + + @objc public var name: String { + root.swiftModel.os!.name + } + + @objc public var version: String { + root.swiftModel.os!.version + } + + @objc public var versionMajor: String { + root.swiftModel.os!.versionMajor + } +} + +@objc +public class DDRUMVitalEventSession: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var hasReplay: NSNumber? { + root.swiftModel.session.hasReplay as NSNumber? + } + + @objc public var id: String { + root.swiftModel.session.id + } + + @objc public var type: DDRUMVitalEventSessionRUMSessionType { + .init(swift: root.swiftModel.session.type) + } +} + +@objc +public enum DDRUMVitalEventSessionRUMSessionType: Int { + internal init(swift: RUMSessionType) { + switch swift { + case .user: self = .user + case .synthetics: self = .synthetics + case .ciTest: self = .ciTest + } + } + + internal var toSwift: RUMSessionType { + switch self { + case .user: return .user + case .synthetics: return .synthetics + case .ciTest: return .ciTest + } + } + + case user + case synthetics + case ciTest +} + +@objc +public enum DDRUMVitalEventSource: Int { + internal init(swift: RUMVitalEvent.Source?) { + switch swift { + case nil: self = .none + case .android?: self = .android + case .ios?: self = .ios + case .browser?: self = .browser + case .flutter?: self = .flutter + case .reactNative?: self = .reactNative + case .roku?: self = .roku + case .unity?: self = .unity + case .kotlinMultiplatform?: self = .kotlinMultiplatform + } + } + + internal var toSwift: RUMVitalEvent.Source? { + switch self { + case .none: return nil + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .roku: return .roku + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case none + case android + case ios + case browser + case flutter + case reactNative + case roku + case unity + case kotlinMultiplatform +} + +@objc +public class DDRUMVitalEventRUMSyntheticsTest: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var injected: NSNumber? { + root.swiftModel.synthetics!.injected as NSNumber? + } + + @objc public var resultId: String { + root.swiftModel.synthetics!.resultId + } + + @objc public var testId: String { + root.swiftModel.synthetics!.testId + } +} + +@objc +public class DDRUMVitalEventRUMUser: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var anonymousId: String? { + root.swiftModel.usr!.anonymousId + } + + @objc public var email: String? { + root.swiftModel.usr!.email + } + + @objc public var id: String? { + root.swiftModel.usr!.id + } + + @objc public var name: String? { + root.swiftModel.usr!.name + } + + @objc public var usrInfo: [String: Any] { + set { root.swiftModel.usr!.usrInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.usr!.usrInfo.dd.objCAttributes } + } +} + +@objc +public class DDRUMVitalEventView: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view.id + } + + @objc public var name: String? { + set { root.swiftModel.view.name = newValue } + get { root.swiftModel.view.name } + } + + @objc public var referrer: String? { + set { root.swiftModel.view.referrer = newValue } + get { root.swiftModel.view.referrer } + } + + @objc public var url: String { + set { root.swiftModel.view.url = newValue } + get { root.swiftModel.view.url } + } +} + +@objc +public class DDRUMVitalEventVital: NSObject { + internal let root: DDRUMVitalEvent + + internal init(root: DDRUMVitalEvent) { + self.root = root + } + + @objc public var custom: [String: NSNumber]? { + 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 + } + + @objc public var name: String? { + root.swiftModel.vital.name + } + + @objc public var type: DDRUMVitalEventVitalVitalType { + .init(swift: root.swiftModel.vital.type) + } +} + +@objc +public enum DDRUMVitalEventVitalVitalType: Int { + internal init(swift: RUMVitalEvent.Vital.VitalType) { + switch swift { + case .duration: self = .duration + } + } + + internal var toSwift: RUMVitalEvent.Vital.VitalType { + switch self { + case .duration: return .duration + } + } + + case duration +} + +@objc +public class DDTelemetryErrorEvent: NSObject { + internal var swiftModel: TelemetryErrorEvent + internal var root: DDTelemetryErrorEvent { self } + + internal init(swiftModel: TelemetryErrorEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDTelemetryErrorEventDD { + DDTelemetryErrorEventDD(root: root) + } + + @objc public var action: DDTelemetryErrorEventAction? { + root.swiftModel.action != nil ? DDTelemetryErrorEventAction(root: root) : nil + } + + @objc public var application: DDTelemetryErrorEventApplication? { + root.swiftModel.application != nil ? DDTelemetryErrorEventApplication(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + + @objc public var experimentalFeatures: [String]? { + root.swiftModel.experimentalFeatures + } + + @objc public var service: String { + root.swiftModel.service + } + + @objc public var session: DDTelemetryErrorEventSession? { + root.swiftModel.session != nil ? DDTelemetryErrorEventSession(root: root) : nil + } + + @objc public var source: DDTelemetryErrorEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var telemetry: DDTelemetryErrorEventTelemetry { + DDTelemetryErrorEventTelemetry(root: root) + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var version: String { + root.swiftModel.version + } + + @objc public var view: DDTelemetryErrorEventView? { + root.swiftModel.view != nil ? DDTelemetryErrorEventView(root: root) : nil + } +} + +@objc +public class DDTelemetryErrorEventDD: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } +} + +@objc +public class DDTelemetryErrorEventAction: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.action!.id + } +} + +@objc +public class DDTelemetryErrorEventApplication: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application!.id + } +} + +@objc +public class DDTelemetryErrorEventSession: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.session!.id + } +} + +@objc +public enum DDTelemetryErrorEventSource: Int { + internal init(swift: TelemetryErrorEvent.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: TelemetryErrorEvent.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case unity + case kotlinMultiplatform +} + +@objc +public class DDTelemetryErrorEventTelemetry: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var device: DDTelemetryErrorEventTelemetryRUMTelemetryDevice? { + root.swiftModel.telemetry.device != nil ? DDTelemetryErrorEventTelemetryRUMTelemetryDevice(root: root) : nil + } + + @objc public var error: DDTelemetryErrorEventTelemetryError? { + root.swiftModel.telemetry.error != nil ? DDTelemetryErrorEventTelemetryError(root: root) : nil + } + + @objc public var message: String { + root.swiftModel.telemetry.message + } + + @objc public var os: DDTelemetryErrorEventTelemetryRUMTelemetryOperatingSystem? { + root.swiftModel.telemetry.os != nil ? DDTelemetryErrorEventTelemetryRUMTelemetryOperatingSystem(root: root) : nil + } + + @objc public var status: String { + root.swiftModel.telemetry.status + } + + @objc public var type: String? { + root.swiftModel.telemetry.type + } + + @objc public var telemetryInfo: [String: Any] { + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } + } +} + +@objc +public class DDTelemetryErrorEventTelemetryRUMTelemetryDevice: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.telemetry.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.telemetry.device!.brand + } + + @objc public var model: String? { + root.swiftModel.telemetry.device!.model + } +} + +@objc +public class DDTelemetryErrorEventTelemetryError: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var kind: String? { + root.swiftModel.telemetry.error!.kind + } + + @objc public var stack: String? { + root.swiftModel.telemetry.error!.stack + } +} + +@objc +public class DDTelemetryErrorEventTelemetryRUMTelemetryOperatingSystem: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.telemetry.os!.build + } + + @objc public var name: String? { + root.swiftModel.telemetry.os!.name + } + + @objc public var version: String? { + root.swiftModel.telemetry.os!.version + } +} + +@objc +public class DDTelemetryErrorEventView: NSObject { + internal let root: DDTelemetryErrorEvent + + internal init(root: DDTelemetryErrorEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view!.id + } +} + +@objc +public class DDTelemetryDebugEvent: NSObject { + internal var swiftModel: TelemetryDebugEvent + internal var root: DDTelemetryDebugEvent { self } + + internal init(swiftModel: TelemetryDebugEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDTelemetryDebugEventDD { + DDTelemetryDebugEventDD(root: root) + } + + @objc public var action: DDTelemetryDebugEventAction? { + root.swiftModel.action != nil ? DDTelemetryDebugEventAction(root: root) : nil + } + + @objc public var application: DDTelemetryDebugEventApplication? { + root.swiftModel.application != nil ? DDTelemetryDebugEventApplication(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + + @objc public var experimentalFeatures: [String]? { + root.swiftModel.experimentalFeatures + } + + @objc public var service: String { + root.swiftModel.service + } + + @objc public var session: DDTelemetryDebugEventSession? { + root.swiftModel.session != nil ? DDTelemetryDebugEventSession(root: root) : nil + } + + @objc public var source: DDTelemetryDebugEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var telemetry: DDTelemetryDebugEventTelemetry { + DDTelemetryDebugEventTelemetry(root: root) + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var version: String { + root.swiftModel.version + } + + @objc public var view: DDTelemetryDebugEventView? { + root.swiftModel.view != nil ? DDTelemetryDebugEventView(root: root) : nil + } +} + +@objc +public class DDTelemetryDebugEventDD: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } +} + +@objc +public class DDTelemetryDebugEventAction: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.action!.id + } +} + +@objc +public class DDTelemetryDebugEventApplication: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application!.id + } +} + +@objc +public class DDTelemetryDebugEventSession: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.session!.id + } +} + +@objc +public enum DDTelemetryDebugEventSource: Int { + internal init(swift: TelemetryDebugEvent.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: TelemetryDebugEvent.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case unity + case kotlinMultiplatform +} + +@objc +public class DDTelemetryDebugEventTelemetry: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var device: DDTelemetryDebugEventTelemetryRUMTelemetryDevice? { + root.swiftModel.telemetry.device != nil ? DDTelemetryDebugEventTelemetryRUMTelemetryDevice(root: root) : nil + } + + @objc public var message: String { + root.swiftModel.telemetry.message + } + + @objc public var os: DDTelemetryDebugEventTelemetryRUMTelemetryOperatingSystem? { + root.swiftModel.telemetry.os != nil ? DDTelemetryDebugEventTelemetryRUMTelemetryOperatingSystem(root: root) : nil + } + + @objc public var status: String { + root.swiftModel.telemetry.status + } + + @objc public var type: String? { + root.swiftModel.telemetry.type + } + + @objc public var telemetryInfo: [String: Any] { + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } + } +} + +@objc +public class DDTelemetryDebugEventTelemetryRUMTelemetryDevice: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.telemetry.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.telemetry.device!.brand + } + + @objc public var model: String? { + root.swiftModel.telemetry.device!.model + } +} + +@objc +public class DDTelemetryDebugEventTelemetryRUMTelemetryOperatingSystem: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.telemetry.os!.build + } + + @objc public var name: String? { + root.swiftModel.telemetry.os!.name + } + + @objc public var version: String? { + root.swiftModel.telemetry.os!.version + } +} + +@objc +public class DDTelemetryDebugEventView: NSObject { + internal let root: DDTelemetryDebugEvent + + internal init(root: DDTelemetryDebugEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view!.id + } +} + +@objc +public class DDTelemetryConfigurationEvent: NSObject { + internal var swiftModel: TelemetryConfigurationEvent + internal var root: DDTelemetryConfigurationEvent { self } + + internal init(swiftModel: TelemetryConfigurationEvent) { + self.swiftModel = swiftModel + } + + @objc public var dd: DDTelemetryConfigurationEventDD { + DDTelemetryConfigurationEventDD(root: root) + } + + @objc public var action: DDTelemetryConfigurationEventAction? { + root.swiftModel.action != nil ? DDTelemetryConfigurationEventAction(root: root) : nil + } + + @objc public var application: DDTelemetryConfigurationEventApplication? { + root.swiftModel.application != nil ? DDTelemetryConfigurationEventApplication(root: root) : nil + } + + @objc public var date: NSNumber { + root.swiftModel.date as NSNumber + } + + @objc public var effectiveSampleRate: NSNumber? { + root.swiftModel.effectiveSampleRate as NSNumber? + } + + @objc public var experimentalFeatures: [String]? { + root.swiftModel.experimentalFeatures + } + + @objc public var service: String { + root.swiftModel.service + } + + @objc public var session: DDTelemetryConfigurationEventSession? { + root.swiftModel.session != nil ? DDTelemetryConfigurationEventSession(root: root) : nil + } + + @objc public var source: DDTelemetryConfigurationEventSource { + .init(swift: root.swiftModel.source) + } + + @objc public var telemetry: DDTelemetryConfigurationEventTelemetry { + DDTelemetryConfigurationEventTelemetry(root: root) + } + + @objc public var type: String { + root.swiftModel.type + } + + @objc public var version: String { + root.swiftModel.version + } + + @objc public var view: DDTelemetryConfigurationEventView? { + root.swiftModel.view != nil ? DDTelemetryConfigurationEventView(root: root) : nil + } +} + +@objc +public class DDTelemetryConfigurationEventDD: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var formatVersion: NSNumber { + root.swiftModel.dd.formatVersion as NSNumber + } +} + +@objc +public class DDTelemetryConfigurationEventAction: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.action!.id + } +} + +@objc +public class DDTelemetryConfigurationEventApplication: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.application!.id + } +} + +@objc +public class DDTelemetryConfigurationEventSession: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.session!.id + } +} + +@objc +public enum DDTelemetryConfigurationEventSource: Int { + internal init(swift: TelemetryConfigurationEvent.Source) { + switch swift { + case .android: self = .android + case .ios: self = .ios + case .browser: self = .browser + case .flutter: self = .flutter + case .reactNative: self = .reactNative + case .unity: self = .unity + case .kotlinMultiplatform: self = .kotlinMultiplatform + } + } + + internal var toSwift: TelemetryConfigurationEvent.Source { + switch self { + case .android: return .android + case .ios: return .ios + case .browser: return .browser + case .flutter: return .flutter + case .reactNative: return .reactNative + case .unity: return .unity + case .kotlinMultiplatform: return .kotlinMultiplatform + } + } + + case android + case ios + case browser + case flutter + case reactNative + case unity + case kotlinMultiplatform +} + +@objc +public class DDTelemetryConfigurationEventTelemetry: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var configuration: DDTelemetryConfigurationEventTelemetryConfiguration { + DDTelemetryConfigurationEventTelemetryConfiguration(root: root) + } + + @objc public var device: DDTelemetryConfigurationEventTelemetryRUMTelemetryDevice? { + root.swiftModel.telemetry.device != nil ? DDTelemetryConfigurationEventTelemetryRUMTelemetryDevice(root: root) : nil + } + + @objc public var os: DDTelemetryConfigurationEventTelemetryRUMTelemetryOperatingSystem? { + root.swiftModel.telemetry.os != nil ? DDTelemetryConfigurationEventTelemetryRUMTelemetryOperatingSystem(root: root) : nil + } + + @objc public var type: String { + root.swiftModel.telemetry.type + } + + @objc public var telemetryInfo: [String: Any] { + set { root.swiftModel.telemetry.telemetryInfo = newValue.dd.swiftAttributes } + get { root.swiftModel.telemetry.telemetryInfo.dd.objCAttributes } + } +} + +@objc +public class DDTelemetryConfigurationEventTelemetryConfiguration: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var actionNameAttribute: String? { + root.swiftModel.telemetry.configuration.actionNameAttribute + } + + @objc public var allowFallbackToLocalStorage: NSNumber? { + root.swiftModel.telemetry.configuration.allowFallbackToLocalStorage as NSNumber? + } + + @objc public var allowUntrustedEvents: NSNumber? { + root.swiftModel.telemetry.configuration.allowUntrustedEvents as NSNumber? + } + + @objc public var appHangThreshold: NSNumber? { + root.swiftModel.telemetry.configuration.appHangThreshold as NSNumber? + } + + @objc public var backgroundTasksEnabled: NSNumber? { + root.swiftModel.telemetry.configuration.backgroundTasksEnabled as NSNumber? + } + + @objc public var batchProcessingLevel: NSNumber? { + root.swiftModel.telemetry.configuration.batchProcessingLevel as NSNumber? + } + + @objc public var batchSize: NSNumber? { + root.swiftModel.telemetry.configuration.batchSize as NSNumber? + } + + @objc public var batchUploadFrequency: NSNumber? { + 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? + } + + @objc public var dartVersion: String? { + set { root.swiftModel.telemetry.configuration.dartVersion = newValue } + get { root.swiftModel.telemetry.configuration.dartVersion } + } + + @objc public var defaultPrivacyLevel: String? { + set { root.swiftModel.telemetry.configuration.defaultPrivacyLevel = newValue } + get { root.swiftModel.telemetry.configuration.defaultPrivacyLevel } + } + + @objc public var enablePrivacyForActionName: NSNumber? { + set { root.swiftModel.telemetry.configuration.enablePrivacyForActionName = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.enablePrivacyForActionName as NSNumber? } + } + + @objc public var forwardConsoleLogs: DDTelemetryConfigurationEventTelemetryConfigurationForwardConsoleLogs? { + root.swiftModel.telemetry.configuration.forwardConsoleLogs != nil ? DDTelemetryConfigurationEventTelemetryConfigurationForwardConsoleLogs(root: root) : nil + } + + @objc public var forwardErrorsToLogs: NSNumber? { + root.swiftModel.telemetry.configuration.forwardErrorsToLogs as NSNumber? + } + + @objc public var forwardReports: DDTelemetryConfigurationEventTelemetryConfigurationForwardReports? { + 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? + } + + @objc public var reactNativeVersion: String? { + set { root.swiftModel.telemetry.configuration.reactNativeVersion = newValue } + get { root.swiftModel.telemetry.configuration.reactNativeVersion } + } + + @objc public var reactVersion: String? { + set { root.swiftModel.telemetry.configuration.reactVersion = newValue } + get { root.swiftModel.telemetry.configuration.reactVersion } + } + + @objc public var replaySampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.replaySampleRate as NSNumber? + } + + @objc public var selectedTracingPropagators: [Int]? { + root.swiftModel.telemetry.configuration.selectedTracingPropagators?.map { DDTelemetryConfigurationEventTelemetryConfigurationSelectedTracingPropagators(swift: $0).rawValue } + } + + @objc public var sendLogsAfterSessionExpiration: NSNumber? { + set { root.swiftModel.telemetry.configuration.sendLogsAfterSessionExpiration = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.sendLogsAfterSessionExpiration as NSNumber? } + } + + @objc public var sessionReplaySampleRate: NSNumber? { + set { root.swiftModel.telemetry.configuration.sessionReplaySampleRate = newValue?.int64Value } + get { root.swiftModel.telemetry.configuration.sessionReplaySampleRate as NSNumber? } + } + + @objc public var sessionSampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.sessionSampleRate as NSNumber? + } + + @objc public var silentMultipleInit: NSNumber? { + 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? } + } + + @objc public var storeContextsAcrossPages: NSNumber? { + root.swiftModel.telemetry.configuration.storeContextsAcrossPages as NSNumber? + } + + @objc public var telemetryConfigurationSampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.telemetryConfigurationSampleRate as NSNumber? + } + + @objc public var telemetrySampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.telemetrySampleRate as NSNumber? + } + + @objc public var telemetryUsageSampleRate: NSNumber? { + 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) } + } + + @objc public var traceSampleRate: NSNumber? { + root.swiftModel.telemetry.configuration.traceSampleRate as NSNumber? + } + + @objc public var tracerApi: String? { + set { root.swiftModel.telemetry.configuration.tracerApi = newValue } + get { root.swiftModel.telemetry.configuration.tracerApi } + } + + @objc public var tracerApiVersion: String? { + set { root.swiftModel.telemetry.configuration.tracerApiVersion = newValue } + get { root.swiftModel.telemetry.configuration.tracerApiVersion } + } + + @objc public var trackBackgroundEvents: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackBackgroundEvents = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackBackgroundEvents as NSNumber? } + } + + @objc public var trackCrossPlatformLongTasks: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackCrossPlatformLongTasks = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackCrossPlatformLongTasks as NSNumber? } + } + + @objc public var trackErrors: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackErrors = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackErrors as NSNumber? } + } + + @objc public var trackFlutterPerformance: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackFlutterPerformance = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackFlutterPerformance as NSNumber? } + } + + @objc public var trackFrustrations: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackFrustrations = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackFrustrations as NSNumber? } + } + + @objc public var trackInteractions: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackInteractions = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackInteractions as NSNumber? } + } + + @objc public var trackLongTask: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackLongTask = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackLongTask as NSNumber? } + } + + @objc public var trackNativeErrors: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackNativeErrors = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackNativeErrors as NSNumber? } + } + + @objc public var trackNativeLongTasks: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackNativeLongTasks = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackNativeLongTasks as NSNumber? } + } + + @objc public var trackNativeViews: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackNativeViews = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackNativeViews as NSNumber? } + } + + @objc public var trackNetworkRequests: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackNetworkRequests = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackNetworkRequests as NSNumber? } + } + + @objc public var trackResources: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackResources = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackResources as NSNumber? } + } + + @objc public var trackSessionAcrossSubdomains: NSNumber? { + root.swiftModel.telemetry.configuration.trackSessionAcrossSubdomains as NSNumber? + } + + @objc public var trackUserInteractions: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackUserInteractions = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackUserInteractions as NSNumber? } + } + + @objc public var trackViewsManually: NSNumber? { + set { root.swiftModel.telemetry.configuration.trackViewsManually = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.trackViewsManually as NSNumber? } + } + + @objc public var trackingConsent: DDTelemetryConfigurationEventTelemetryConfigurationTrackingConsent { + .init(swift: root.swiftModel.telemetry.configuration.trackingConsent) + } + + @objc public var unityVersion: String? { + set { root.swiftModel.telemetry.configuration.unityVersion = newValue } + get { root.swiftModel.telemetry.configuration.unityVersion } + } + + @objc public var useAllowedTracingOrigins: NSNumber? { + root.swiftModel.telemetry.configuration.useAllowedTracingOrigins as NSNumber? + } + + @objc public var useAllowedTracingUrls: NSNumber? { + root.swiftModel.telemetry.configuration.useAllowedTracingUrls as NSNumber? + } + + @objc public var useBeforeSend: NSNumber? { + root.swiftModel.telemetry.configuration.useBeforeSend as NSNumber? + } + + @objc public var useCrossSiteSessionCookie: NSNumber? { + root.swiftModel.telemetry.configuration.useCrossSiteSessionCookie as NSNumber? + } + + @objc public var useExcludedActivityUrls: NSNumber? { + root.swiftModel.telemetry.configuration.useExcludedActivityUrls as NSNumber? + } + + @objc public var useFirstPartyHosts: NSNumber? { + set { root.swiftModel.telemetry.configuration.useFirstPartyHosts = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.useFirstPartyHosts as NSNumber? } + } + + @objc public var useLocalEncryption: NSNumber? { + root.swiftModel.telemetry.configuration.useLocalEncryption as NSNumber? + } + + @objc public var usePartitionedCrossSiteSessionCookie: NSNumber? { + root.swiftModel.telemetry.configuration.usePartitionedCrossSiteSessionCookie as NSNumber? + } + + @objc public var usePciIntake: NSNumber? { + set { root.swiftModel.telemetry.configuration.usePciIntake = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.usePciIntake as NSNumber? } + } + + @objc public var useProxy: NSNumber? { + set { root.swiftModel.telemetry.configuration.useProxy = newValue?.boolValue } + get { root.swiftModel.telemetry.configuration.useProxy as NSNumber? } + } + + @objc public var useSecureSessionCookie: NSNumber? { + root.swiftModel.telemetry.configuration.useSecureSessionCookie as NSNumber? + } + + @objc public var useTracing: NSNumber? { + root.swiftModel.telemetry.configuration.useTracing as NSNumber? + } + + @objc public var useWorkerUrl: NSNumber? { + root.swiftModel.telemetry.configuration.useWorkerUrl as NSNumber? + } + + @objc public var viewTrackingStrategy: DDTelemetryConfigurationEventTelemetryConfigurationViewTrackingStrategy { + .init(swift: root.swiftModel.telemetry.configuration.viewTrackingStrategy) + } +} + +@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 + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var stringsArray: [String]? { + guard case .stringsArray(let value) = root.swiftModel.telemetry.configuration.forwardConsoleLogs else { + return nil + } + return value + } + + @objc public var string: String? { + guard case .string(let value) = root.swiftModel.telemetry.configuration.forwardConsoleLogs else { + return nil + } + return value + } +} + +@objc +public class DDTelemetryConfigurationEventTelemetryConfigurationForwardReports: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var stringsArray: [String]? { + guard case .stringsArray(let value) = root.swiftModel.telemetry.configuration.forwardReports else { + return nil + } + return value + } + + @objc public var string: String? { + guard case .string(let value) = root.swiftModel.telemetry.configuration.forwardReports else { + return nil + } + return value + } +} + +@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?) { + switch swift { + case nil: self = .none + case .datadog?: self = .datadog + case .b3?: self = .b3 + case .b3multi?: self = .b3multi + case .tracecontext?: self = .tracecontext + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.SelectedTracingPropagators? { + switch self { + case .none: return nil + case .datadog: return .datadog + case .b3: return .b3 + case .b3multi: return .b3multi + case .tracecontext: return .tracecontext + } + } + + case none + case datadog + case b3 + case b3multi + case tracecontext +} + +@objc +public enum DDTelemetryConfigurationEventTelemetryConfigurationTraceContextInjection: Int { + internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.TraceContextInjection?) { + switch swift { + case nil: self = .none + case .all?: self = .all + case .sampled?: self = .sampled + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.TraceContextInjection? { + switch self { + case .none: return nil + case .all: return .all + case .sampled: return .sampled + } + } + + case none + case all + case sampled +} + +@objc +public enum DDTelemetryConfigurationEventTelemetryConfigurationTrackingConsent: Int { + internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.TrackingConsent?) { + switch swift { + case nil: self = .none + case .granted?: self = .granted + case .notGranted?: self = .notGranted + case .pending?: self = .pending + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.TrackingConsent? { + switch self { + case .none: return nil + case .granted: return .granted + case .notGranted: return .notGranted + case .pending: return .pending + } + } + + case none + case granted + case notGranted + case pending +} + +@objc +public enum DDTelemetryConfigurationEventTelemetryConfigurationViewTrackingStrategy: Int { + internal init(swift: TelemetryConfigurationEvent.Telemetry.Configuration.ViewTrackingStrategy?) { + switch swift { + case nil: self = .none + case .activityViewTrackingStrategy?: self = .activityViewTrackingStrategy + case .fragmentViewTrackingStrategy?: self = .fragmentViewTrackingStrategy + case .mixedViewTrackingStrategy?: self = .mixedViewTrackingStrategy + case .navigationViewTrackingStrategy?: self = .navigationViewTrackingStrategy + } + } + + internal var toSwift: TelemetryConfigurationEvent.Telemetry.Configuration.ViewTrackingStrategy? { + switch self { + case .none: return nil + case .activityViewTrackingStrategy: return .activityViewTrackingStrategy + case .fragmentViewTrackingStrategy: return .fragmentViewTrackingStrategy + case .mixedViewTrackingStrategy: return .mixedViewTrackingStrategy + case .navigationViewTrackingStrategy: return .navigationViewTrackingStrategy + } + } + + case none + case activityViewTrackingStrategy + case fragmentViewTrackingStrategy + case mixedViewTrackingStrategy + case navigationViewTrackingStrategy +} + +@objc +public class DDTelemetryConfigurationEventTelemetryRUMTelemetryDevice: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var architecture: String? { + root.swiftModel.telemetry.device!.architecture + } + + @objc public var brand: String? { + root.swiftModel.telemetry.device!.brand + } + + @objc public var model: String? { + root.swiftModel.telemetry.device!.model + } +} + +@objc +public class DDTelemetryConfigurationEventTelemetryRUMTelemetryOperatingSystem: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var build: String? { + root.swiftModel.telemetry.os!.build + } + + @objc public var name: String? { + root.swiftModel.telemetry.os!.name + } + + @objc public var version: String? { + root.swiftModel.telemetry.os!.version + } +} + +@objc +public class DDTelemetryConfigurationEventView: NSObject { + internal let root: DDTelemetryConfigurationEvent + + internal init(root: DDTelemetryConfigurationEvent) { + self.root = root + } + + @objc public var id: String { + root.swiftModel.view!.id + } +} + +// swiftlint:enable force_unwrapping + +// Generated from https://github.com/DataDog/rum-events-format/tree/81c3d7401cba2a2faf48b5f4c0e8aca05c759662 diff --git a/Sources/DatadogObjc/Tracing/DDSpan+objc.swift b/DatadogObjc/Sources/Tracing/DDSpan+objc.swift similarity index 78% rename from Sources/DatadogObjc/Tracing/DDSpan+objc.swift rename to DatadogObjc/Sources/Tracing/DDSpan+objc.swift index 15a1ab1bf2..fbfdcf63b2 100644 --- a/Sources/DatadogObjc/Tracing/DDSpan+objc.swift +++ b/DatadogObjc/Sources/Tracing/DDSpan+objc.swift @@ -1,15 +1,17 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ -import protocol Datadog.OTSpan +import Foundation +import DatadogInternal +import DatadogTrace internal class DDSpanObjc: NSObject, DatadogObjc.OTSpan { - let swiftSpan: Datadog.OTSpan + let swiftSpan: DatadogTrace.OTSpan - init(objcTracer: DatadogObjc.OTTracer, swiftSpan: Datadog.OTSpan) { + init(objcTracer: DatadogObjc.OTTracer, swiftSpan: DatadogTrace.OTSpan) { self.tracer = objcTracer self.context = DDSpanContextObjc(swiftSpanContext: swiftSpan.context) self.swiftSpan = swiftSpan @@ -63,6 +65,14 @@ internal class DDSpanObjc: NSObject, DatadogObjc.OTSpan { return swiftSpan.baggageItem(withKey: key) } + func setError(_ error: Error) { + swiftSpan.setError(error) + } + + func setError(kind: String, message: String, stack: String?) { + swiftSpan.setError(kind: kind, message: message, stack: stack ?? "") + } + func finish() { swiftSpan.finish() } @@ -74,4 +84,9 @@ internal class DDSpanObjc: NSObject, DatadogObjc.OTSpan { swiftSpan.finish() } } + + func setActive() -> DatadogObjc.OTSpan { + _ = swiftSpan.setActive() + return self + } } diff --git a/Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift b/DatadogObjc/Sources/Tracing/DDSpanContext+objc.swift similarity index 76% rename from Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift rename to DatadogObjc/Sources/Tracing/DDSpanContext+objc.swift index 16478c40e7..1d922795da 100644 --- a/Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift +++ b/DatadogObjc/Sources/Tracing/DDSpanContext+objc.swift @@ -1,15 +1,16 @@ /* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. + * Copyright 2019-Present Datadog, Inc. */ -import protocol Datadog.OTSpanContext +import Foundation +import DatadogTrace internal class DDSpanContextObjc: NSObject, OTSpanContext { - let swiftSpanContext: Datadog.OTSpanContext + let swiftSpanContext: DatadogTrace.OTSpanContext - internal init(swiftSpanContext: Datadog.OTSpanContext) { + internal init(swiftSpanContext: DatadogTrace.OTSpanContext) { self.swiftSpanContext = swiftSpanContext } diff --git a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift new file mode 100644 index 0000000000..3919197e4d --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import class DatadogInternal.B3HTTPHeadersWriter + +@objc +public enum DDInjectEncoding: Int { + case multiple = 0 + case single = 1 +} + +private extension B3HTTPHeadersWriter.InjectEncoding { + init(_ value: DDInjectEncoding) { + switch value { + case .single: + self = .single + case .multiple: + self = .multiple + } + } +} + +@objc +@available(*, deprecated, renamed: "DDB3HTTPHeadersWriter") +public class DDOTelHTTPHeadersWriter: DDB3HTTPHeadersWriter {} + +@objc +public class DDB3HTTPHeadersWriter: NSObject { + let swiftB3HTTPHeadersWriter: B3HTTPHeadersWriter + + @objc public var traceHeaderFields: [String: String] { + swiftB3HTTPHeadersWriter.traceHeaderFields + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init( + samplingRate: Float, + injectEncoding: DDInjectEncoding = .single + ) { + self.init(sampleRate: samplingRate, injectEncoding: injectEncoding) + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public init( + sampleRate: Float = 20, + injectEncoding: DDInjectEncoding = .single + ) { + swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + injectEncoding: .init(injectEncoding), + traceContextInjection: .all + ) + } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy, + injectEncoding: DDInjectEncoding = .single, + traceContextInjection: DDTraceContextInjection = .all + ) { + swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + injectEncoding: .init(injectEncoding), + traceContextInjection: traceContextInjection.swiftType + ) + } +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift new file mode 100644 index 0000000000..c5e9ccbf1e --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -0,0 +1,43 @@ +/* + * 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 class DatadogInternal.HTTPHeadersWriter + +@objc +public class DDHTTPHeadersWriter: NSObject { + let swiftHTTPHeadersWriter: HTTPHeadersWriter + + @objc public var traceHeaderFields: [String: String] { + swiftHTTPHeadersWriter.traceHeaderFields + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(samplingRate: Float) { + self.init(sampleRate: samplingRate) + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public init(sampleRate: Float = 20) { + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + traceContextInjection: .all + ) + } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy, + traceContextInjection: DDTraceContextInjection + ) { + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + traceContextInjection: traceContextInjection.swiftType + ) + } +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift new file mode 100644 index 0000000000..829e5f92ac --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/TraceContextInjection+objc.swift @@ -0,0 +1,27 @@ +/* + * 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 + +/// Defines whether the trace context should be injected into all requests or only sampled ones. +@objc +public enum DDTraceContextInjection: Int { + internal var swiftType: DatadogInternal.TraceContextInjection { + switch self { + case .all: + return .all + case .sampled: + return .sampled + } + } + + /// Injects trace context into all requests irrespective of the sampling decision. + case all + + /// Injects trace context only into sampled requests. + case sampled +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift new file mode 100644 index 0000000000..fc1c5b081c --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.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 +import DatadogInternal + +/// Available strategies for sampling trace propagation headers. +@objc +public class DDTraceSamplingStrategy: NSObject { + internal let swiftType: DatadogInternal.TraceSamplingStrategy + + /// Trace propagation headers will be sampled same as propagated span. + /// + /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` + /// strategy, this decision is propagated through the request context to downstream services. + @objc + public static func headBased() -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .headBased) + } + + /// Trace propagation headers will be sampled independently from sampling decision in propagated span. + /// + /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace + /// in downstream services independently of sampling their parent span. + @objc + public static func custom(sampleRate: Float) -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .custom(sampleRate: sampleRate)) + } + + private init(swiftType: DatadogInternal.TraceSamplingStrategy) { + self.swiftType = swiftType + } +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift new file mode 100644 index 0000000000..13841a2c7f --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.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 class DatadogInternal.W3CHTTPHeadersWriter + +@objc +public class DDW3CHTTPHeadersWriter: NSObject { + let swiftW3CHTTPHeadersWriter: W3CHTTPHeadersWriter + + @objc public var traceHeaderFields: [String: String] { + swiftW3CHTTPHeadersWriter.traceHeaderFields + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public convenience init(samplingRate: Float) { + self.init(sampleRate: samplingRate) + } + + @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") + public init(sampleRate: Float = 20) { + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + tracestate: [:], + traceContextInjection: .all + ) + } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy, + traceContextInjection: DDTraceContextInjection + ) { + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + tracestate: [:], + traceContextInjection: traceContextInjection.swiftType + ) + } +} diff --git a/DatadogObjc/Sources/Tracing/Trace+objc.swift b/DatadogObjc/Sources/Tracing/Trace+objc.swift new file mode 100644 index 0000000000..612c2ce4d3 --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Trace+objc.swift @@ -0,0 +1,257 @@ +/* + * 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 DatadogTrace + +@objc +public class DDTraceConfiguration: NSObject { + internal var swiftConfig: Trace.Configuration + + @objc + override public init() { + swiftConfig = .init() + } + + @objc public var sampleRate: Float { + set { swiftConfig.sampleRate = newValue } + get { swiftConfig.sampleRate } + } + + @objc public var service: String? { + set { swiftConfig.service = newValue } + get { swiftConfig.service } + } + + @objc public var tags: [String: Any]? { + set { swiftConfig.tags = newValue?.dd.swiftAttributes } + get { swiftConfig.tags?.dd.objCAttributes } + } + + @objc + public func setURLSessionTracking(_ tracking: DDTraceURLSessionTracking) { + swiftConfig.urlSessionTracking = tracking.swiftConfig + } + + @objc public var bundleWithRumEnabled: Bool { + set { swiftConfig.bundleWithRumEnabled = newValue } + get { swiftConfig.bundleWithRumEnabled } + } + + @objc public var networkInfoEnabled: Bool { + set { swiftConfig.networkInfoEnabled = newValue } + get { swiftConfig.networkInfoEnabled } + } + + @objc public var customEndpoint: URL? { + set { swiftConfig.customEndpoint = newValue } + get { swiftConfig.customEndpoint } + } +} + +@objc +public class DDTraceFirstPartyHostsTracing: NSObject { + internal var swiftType: Trace.Configuration.URLSessionTracking.FirstPartyHostsTracing + + @objc + public init(hostsWithHeaderTypes: [String: Set]) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in Set(headerTypes.map { $0.swiftType }) } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders) + } + + @objc + public init(hostsWithHeaderTypes: [String: Set], sampleRate: Float) { + let swiftHostsWithHeaders = hostsWithHeaderTypes.mapValues { headerTypes in Set(headerTypes.map { $0.swiftType }) } + swiftType = .traceWithHeaders(hostsWithHeaders: swiftHostsWithHeaders, sampleRate: sampleRate) + } + + @objc + public init(hosts: Set) { + swiftType = .trace(hosts: hosts) + } + + @objc + public init(hosts: Set, sampleRate: Float) { + swiftType = .trace(hosts: hosts, sampleRate: sampleRate) + } +} + +@objc +public class DDTraceURLSessionTracking: NSObject { + internal var swiftConfig: Trace.Configuration.URLSessionTracking + + @objc + public init(firstPartyHostsTracing: DDTraceFirstPartyHostsTracing) { + swiftConfig = .init(firstPartyHostsTracing: firstPartyHostsTracing.swiftType) + } + + @objc + public func setFirstPartyHostsTracing(_ firstPartyHostsTracing: DDTraceFirstPartyHostsTracing) { + swiftConfig.firstPartyHostsTracing = firstPartyHostsTracing.swiftType + } +} + +@objc +public class DDTrace: NSObject { + @objc + public static func enable(with configuration: DDTraceConfiguration) { + Trace.enable(with: configuration.swiftConfig) + } +} + +@objc +public class DDTracer: NSObject, DatadogObjc.OTTracer { + // MARK: - Internal + + internal let swiftTracer: DatadogTrace.OTTracer + + internal init(swiftTracer: DatadogTrace.OTTracer) { + self.swiftTracer = swiftTracer + } + + // MARK: - Public + + @objc + public static func shared() -> DatadogObjc.OTTracer { + DDTracer(swiftTracer: Tracer.shared()) + } + + @objc + public func startSpan(_ operationName: String) -> OTSpan { + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan(operationName: operationName) + ) + } + + @objc + public func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan { + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + tags: tags.flatMap { castTagsToSwift($0) } + ) + ) + } + + @objc + public func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext + ) + ) + } + + @objc + public func startSpan( + _ operationName: String, + childOf parent: OTSpanContext?, + tags: NSDictionary? + ) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext, + tags: tags.flatMap { castTagsToSwift($0) } + ) + ) + } + + @objc + public func startSpan( + _ operationName: String, + childOf parent: OTSpanContext?, + tags: NSDictionary?, + startTime: Date? + ) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext, + tags: tags.flatMap { castTagsToSwift($0) }, + startTime: startTime + ) + ) + } + + @objc + public func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws { + if let objcWriter = carrier as? DDHTTPHeadersWriter, format == OT.formatTextMap { + guard let ddspanContext = spanContext.dd else { + return + } + swiftTracer.inject( + spanContext: ddspanContext.swiftSpanContext, + writer: objcWriter.swiftHTTPHeadersWriter + ) + } else if let objcWriter = carrier as? DDB3HTTPHeadersWriter, format == OT.formatTextMap { + guard let ddspanContext = spanContext.dd else { + return + } + swiftTracer.inject( + spanContext: ddspanContext.swiftSpanContext, + writer: objcWriter.swiftB3HTTPHeadersWriter + ) + } else if let objcWriter = carrier as? DDW3CHTTPHeadersWriter, format == OT.formatTextMap { + guard let ddspanContext = spanContext.dd else { + return + } + swiftTracer.inject( + spanContext: ddspanContext.swiftSpanContext, + writer: objcWriter.swiftW3CHTTPHeadersWriter + ) + } else { + let error = NSError( + domain: "DDTracer", + code: 0, + userInfo: [ + NSLocalizedDescriptionKey: "Trying to inject `OTSpanContext` using wrong format and/or carrier.", + NSLocalizedRecoverySuggestionErrorKey: "Use `DDHTTPHeadersWriter` carrier with `OT.formatTextMap` format." + ] + ) + throw error + } + } + + @objc + public func extractWithFormat(_ format: String, carrier: Any) throws { + // TODO: RUMM-385 - we don't need to support it now + } + + // MARK: - Private + + private func castTagsToSwift(_ tags: NSDictionary) -> [String: Encodable] { + var validTags: [String: Encodable] = [:] + + tags.forEach { tagKey, tagValue in + if let stringKey = tagKey as? String { + let encodableValue: Encodable = { + if let stringValue = tagValue as? String { + return stringValue + } else if let urlValue = tagValue as? URL { + return urlValue + } else { + return AnyEncodable(tagValue) + } + }() + + validTags[stringKey] = encodableValue + } + } + + return validTags + } +} diff --git a/DatadogObjc/Sources/Tracing/Utils/Casting.swift b/DatadogObjc/Sources/Tracing/Utils/Casting.swift new file mode 100644 index 0000000000..110bd79cfb --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Utils/Casting.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 DatadogTrace + +// swiftlint:disable identifier_name +internal extension DatadogObjc.OTTracer { + var dd: DDTracer? { warnIfCannotCast(value: self) } +} +internal extension DatadogObjc.OTSpan { + var dd: DDSpanObjc? { warnIfCannotCast(value: self) } +} +internal extension DatadogObjc.OTSpanContext { + var dd: DDSpanContextObjc? { warnIfCannotCast(value: self) } +} +// swiftlint:enable identifier_name + +/// Returns `nil` if the warning was raised. `T` otherwise. +private func warnIfCannotCast(value: Any) -> T? { + guard let castedValue = value as? T else { + print("🔥 Using \(type(of: value as Any)) while \(T.self) was expected.") + return nil + } + return castedValue +} diff --git a/DatadogRUM.podspec b/DatadogRUM.podspec new file mode 100644 index 0000000000..4992c12dec --- /dev/null +++ b/DatadogRUM.podspec @@ -0,0 +1,31 @@ +Pod::Spec.new do |s| + s.name = "DatadogRUM" + s.version = "2.22.0" + s.summary = "Datadog Real User Monitoring Module." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Maxime Epain" => "maxime.epain@datadoghq.com", + "Ganesh Jangir" => "ganesh.jangir@datadoghq.com", + "Maciej Burda" => "maciej.burda@datadoghq.com" + } + + s.swift_version = '5.9' + s.ios.deployment_target = '12.0' + s.tvos.deployment_target = '12.0' + + s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } + + s.source_files = ["DatadogRUM/Sources/**/*.swift"] + + s.resource_bundle = { + "DatadogRUM" => "DatadogRUM/Resources/PrivacyInfo.xcprivacy" + } + + s.dependency 'DatadogInternal', s.version.to_s + +end diff --git a/DatadogRUM/Resources/PrivacyInfo.xcprivacy b/DatadogRUM/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..9ebea9e428 --- /dev/null +++ b/DatadogRUM/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,21 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift new file mode 100644 index 0000000000..ecdb28fe44 --- /dev/null +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -0,0 +1,4983 @@ +/* + * 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 DatadogInternal + +// This file was generated from JSON Schema. Do not modify it directly. + +internal protocol RUMDataModel: Codable {} + +/// Schema of all properties of an Action event +public struct RUMActionEvent: RUMDataModel { + /// Internal properties + public var dd: DD + + /// Action properties + public var action: Action + + /// Application properties + public let application: Application + + /// Generated unique ID of the application build. Unlike version or build_version this field is not meant to be coming from the user, but rather generated by the tooling for each build. + public let buildId: String? + + /// The build version for this application + public let buildVersion: String? + + /// CI Visibility properties + public let ciTest: RUMCITest? + + /// Device connectivity properties + public let connectivity: RUMConnectivity? + + /// View Container properties (view wrapping the current view) + public let container: Container? + + /// User provided context + public var context: RUMEventAttributes? + + /// Start of the event in ms from epoch + public let date: Int64 + + /// Device properties + public let device: RUMDevice? + + /// Display properties + public let display: Display? + + /// Operating system properties + public let os: RUMOperatingSystem? + + /// The service name for this application + public let service: String? + + /// Session properties + public let session: Session + + /// The source of this event + public let source: Source? + + /// Synthetics properties + public let synthetics: RUMSyntheticsTest? + + /// RUM event type + public let type: String = "action" + + /// User properties + public var usr: RUMUser? + + /// The version for this application + public let version: String? + + /// View properties + public var view: View + + enum CodingKeys: String, CodingKey { + case dd = "_dd" + case action = "action" + case application = "application" + case buildId = "build_id" + case buildVersion = "build_version" + case ciTest = "ci_test" + case connectivity = "connectivity" + case container = "container" + case context = "context" + case date = "date" + case device = "device" + case display = "display" + case os = "os" + case service = "service" + case session = "session" + case source = "source" + case synthetics = "synthetics" + case type = "type" + case usr = "usr" + case version = "version" + case view = "view" + } + + /// Internal properties + public struct DD: Codable { + /// Action properties + public var action: Action? + + /// Browser SDK version + public let browserSdkVersion: String? + + /// Subset of the SDK configuration options in use during its execution + public let configuration: Configuration? + + /// Version of the RUM event format + public let formatVersion: Int64 = 2 + + /// Session-related internal properties + public let session: Session? + + enum CodingKeys: String, CodingKey { + case action = "action" + case browserSdkVersion = "browser_sdk_version" + case configuration = "configuration" + case formatVersion = "format_version" + case session = "session" + } + + /// 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? + + /// Target properties + 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) + public let x: Int64 + + /// Y coordinate relative to the target element of the action (in pixels) + public let y: Int64 + + enum CodingKeys: String, CodingKey { + case x = "x" + case y = "y" + } + } + + /// Target properties + public struct Target: Codable { + /// Height of the target element (in pixels) + public let height: Int64? + + /// CSS selector path of the target element + public let selector: String? + + /// Width of the target element (in pixels) + public let width: Int64? + + enum CodingKeys: String, CodingKey { + case height = "height" + case selector = "selector" + case width = "width" + } + } + } + + /// Subset of the SDK configuration options in use during its execution + public struct Configuration: Codable { + /// The percentage of sessions with RUM & Session Replay pricing tracked + public let sessionReplaySampleRate: Double? + + /// The percentage of sessions tracked + public let sessionSampleRate: Double + + enum CodingKeys: String, CodingKey { + case sessionReplaySampleRate = "session_replay_sample_rate" + case sessionSampleRate = "session_sample_rate" + } + } + + /// Session-related internal properties + public struct Session: Codable { + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? + + /// The precondition that led to the creation of the session + public let sessionPrecondition: RUMSessionPrecondition? + + enum CodingKeys: String, CodingKey { + case plan = "plan" + case sessionPrecondition = "session_precondition" + } + + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public enum Plan: Int, Codable { + case plan1 = 1 + case plan2 = 2 + } + } + } + + /// Action properties + public struct Action: Codable { + /// Properties of the crashes of the action + public let crash: Crash? + + /// Properties of the errors of the action + public let error: Error? + + /// Action frustration properties + public let frustration: Frustration? + + /// UUID of the action + public let id: String? + + /// Duration in ns to the action is considered loaded + public let loadingTime: Int64? + + /// Properties of the long tasks of the action + public let longTask: LongTask? + + /// Properties of the resources of the action + public let resource: Resource? + + /// Action target properties + public var target: Target? + + /// Type of the action + public let type: ActionType + + enum CodingKeys: String, CodingKey { + case crash = "crash" + case error = "error" + case frustration = "frustration" + case id = "id" + case loadingTime = "loading_time" + case longTask = "long_task" + case resource = "resource" + case target = "target" + case type = "type" + } + + /// Properties of the crashes of the action + public struct Crash: Codable { + /// Number of crashes that occurred on the action + public let count: Int64 + + enum CodingKeys: String, CodingKey { + case count = "count" + } + } + + /// Properties of the errors of the action + public struct Error: Codable { + /// Number of errors that occurred on the action + public let count: Int64 + + enum CodingKeys: String, CodingKey { + case count = "count" + } + } + + /// Action frustration properties + public struct Frustration: Codable { + /// Action frustration types + public let type: [FrustrationType] + + enum CodingKeys: String, CodingKey { + case type = "type" + } + + public enum FrustrationType: String, Codable { + case rageClick = "rage_click" + case deadClick = "dead_click" + case errorClick = "error_click" + case rageTap = "rage_tap" + case errorTap = "error_tap" + } + } + + /// Properties of the long tasks of the action + public struct LongTask: Codable { + /// Number of long tasks that occurred on the action + public let count: Int64 + + enum CodingKeys: String, CodingKey { + case count = "count" + } + } + + /// Properties of the resources of the action + public struct Resource: Codable { + /// Number of resources that occurred on the action + public let count: Int64 + + enum CodingKeys: String, CodingKey { + case count = "count" + } + } + + /// Action target properties + public struct Target: Codable { + /// Target name + public var name: String + + enum CodingKeys: String, CodingKey { + case name = "name" + } + } + + /// Type of the action + public enum ActionType: String, Codable { + case custom = "custom" + case click = "click" + case tap = "tap" + case scroll = "scroll" + case swipe = "swipe" + case applicationStart = "application_start" + case back = "back" + } + } + + /// Application properties + public struct Application: Codable { + /// UUID of the application + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + + /// View Container properties (view wrapping the current view) + public struct Container: Codable { + /// Source of the parent view + public let source: Source + + /// Attributes of the view's container + public let view: View + + enum CodingKeys: String, CodingKey { + case source = "source" + case view = "view" + } + + /// Source of the parent view + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" + } + + /// Attributes of the view's container + public struct View: Codable { + /// ID of the parent view + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + } + + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + + /// Session properties + public struct Session: Codable { + /// Whether this session has a replay + public let hasReplay: Bool? + + /// UUID of the session + public let id: String + + /// Type of the session + public let type: RUMSessionType + + enum CodingKeys: String, CodingKey { + case hasReplay = "has_replay" + case id = "id" + case type = "type" + } + } + + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" + } + + /// View properties + public struct View: Codable { + /// UUID of the view + public let id: String + + /// Is the action starting in the foreground (focus in browser) + public let inForeground: Bool? + + /// User defined name of the view + public var name: String? + + /// URL that linked to the initial view of the page + public var referrer: String? + + /// URL of the view + public var url: String + + enum CodingKeys: String, CodingKey { + case id = "id" + case inForeground = "in_foreground" + case name = "name" + case referrer = "referrer" + case url = "url" + } + } +} + +/// Schema of all properties of an Error event +public struct RUMErrorEvent: RUMDataModel { + /// Internal properties + public let dd: DD + + /// Action properties + public let action: Action? + + /// Application properties + public let application: Application + + /// Generated unique ID of the application build. Unlike version or build_version this field is not meant to be coming from the user, but rather generated by the tooling for each build. + public let buildId: String? + + /// The build version for this application + public let buildVersion: String? + + /// CI Visibility properties + public let ciTest: RUMCITest? + + /// Device connectivity properties + public let connectivity: RUMConnectivity? + + /// View Container properties (view wrapping the current view) + public let container: Container? + + /// User provided context + public var context: RUMEventAttributes? + + /// Start of the event in ms from epoch + public let date: Int64 + + /// Device properties + public let device: RUMDevice? + + /// Display properties + public let display: Display? + + /// Error properties + public var error: Error + + /// Feature flags properties + public var featureFlags: FeatureFlags? + + /// Properties of App Hang and ANR errors + public let freeze: Freeze? + + /// Operating system properties + public let os: RUMOperatingSystem? + + /// The service name for this application + public let service: String? + + /// Session properties + public let session: Session + + /// The source of this event + public let source: Source? + + /// Synthetics properties + public let synthetics: RUMSyntheticsTest? + + /// RUM event type + public let type: String = "error" + + /// User properties + public var usr: RUMUser? + + /// The version for this application + public let version: String? + + /// View properties + public var view: View + + enum CodingKeys: String, CodingKey { + case dd = "_dd" + case action = "action" + case application = "application" + case buildId = "build_id" + case buildVersion = "build_version" + case ciTest = "ci_test" + case connectivity = "connectivity" + case container = "container" + case context = "context" + case date = "date" + case device = "device" + case display = "display" + case error = "error" + case featureFlags = "feature_flags" + case freeze = "freeze" + case os = "os" + case service = "service" + case session = "session" + case source = "source" + case synthetics = "synthetics" + case type = "type" + case usr = "usr" + case version = "version" + case view = "view" + } + + /// Internal properties + public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + + /// Subset of the SDK configuration options in use during its execution + public let configuration: Configuration? + + /// Version of the RUM event format + public let formatVersion: Int64 = 2 + + /// Session-related internal properties + public let session: Session? + + enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" + case configuration = "configuration" + case formatVersion = "format_version" + case session = "session" + } + + /// Subset of the SDK configuration options in use during its execution + public struct Configuration: Codable { + /// The percentage of sessions with RUM & Session Replay pricing tracked + public let sessionReplaySampleRate: Double? + + /// The percentage of sessions tracked + public let sessionSampleRate: Double + + enum CodingKeys: String, CodingKey { + case sessionReplaySampleRate = "session_replay_sample_rate" + case sessionSampleRate = "session_sample_rate" + } + } + + /// Session-related internal properties + public struct Session: Codable { + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? + + /// The precondition that led to the creation of the session + public let sessionPrecondition: RUMSessionPrecondition? + + enum CodingKeys: String, CodingKey { + case plan = "plan" + case sessionPrecondition = "session_precondition" + } + + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public enum Plan: Int, Codable { + case plan1 = 1 + case plan2 = 2 + } + } + } + + /// Action properties + public struct Action: Codable { + /// UUID of the action + public let id: RUMActionID + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + + /// Application properties + public struct Application: Codable { + /// UUID of the application + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + + /// View Container properties (view wrapping the current view) + public struct Container: Codable { + /// Source of the parent view + public let source: Source + + /// Attributes of the view's container + public let view: View + + enum CodingKeys: String, CodingKey { + case source = "source" + case view = "view" + } + + /// Source of the parent view + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" + } + + /// Attributes of the view's container + public struct View: Codable { + /// ID of the parent view + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + } + + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + + /// Error properties + public struct Error: Codable { + /// Description of each binary image (native libraries; for Android: .so files) loaded or referenced by the process/application. + public let binaryImages: [BinaryImages]? + + /// The specific category of the error. It provides a high-level grouping for different types of errors. + public let category: Category? + + /// Causes of the error + public var causes: [Causes]? + + /// Content Security Violation properties + public let csp: CSP? + + /// Fingerprint used for Error Tracking custom grouping + public var fingerprint: String? + + /// Whether the error has been handled manually in the source code or not + public let handling: Handling? + + /// Handling call stack + public let handlingStack: String? + + /// UUID of the error + public let id: String? + + /// Whether this error crashed the host application + public let isCrash: Bool? + + /// Error message + public var message: String + + /// Platform-specific metadata of the error event. + public let meta: Meta? + + /// Resource properties of the error + public var resource: Resource? + + /// Source of the error + public let source: Source + + /// Source type of the error (the language or platform impacting the error stacktrace format) + public let sourceType: SourceType? + + /// Stacktrace of the error + public var stack: String? + + /// Description of each thread in the process when error happened. + public let threads: [Threads]? + + /// Time since application start when error happened (in milliseconds) + public let timeSinceAppStart: Int64? + + /// The type of the error + public let type: String? + + /// A boolean value saying if any of the stack traces was truncated due to minification. + public let wasTruncated: Bool? + + enum CodingKeys: String, CodingKey { + case binaryImages = "binary_images" + case category = "category" + case causes = "causes" + case csp = "csp" + case fingerprint = "fingerprint" + case handling = "handling" + case handlingStack = "handling_stack" + case id = "id" + case isCrash = "is_crash" + case message = "message" + case meta = "meta" + case resource = "resource" + case source = "source" + case sourceType = "source_type" + case stack = "stack" + case threads = "threads" + case timeSinceAppStart = "time_since_app_start" + case type = "type" + case wasTruncated = "was_truncated" + } + + /// Description of the binary image (native library; for Android: .so file) loaded or referenced by the process/application. + public struct BinaryImages: Codable { + /// CPU architecture from the library. + public let arch: String? + + /// Determines if it's a system or user library. + public let isSystem: Bool + + /// Library's load address (hexadecimal). + public let loadAddress: String? + + /// Max value from the library address range (hexadecimal). + public let maxAddress: String? + + /// Name of the library. + public let name: String + + /// Build UUID that uniquely identifies the binary image. + public let uuid: String + + enum CodingKeys: String, CodingKey { + case arch = "arch" + case isSystem = "is_system" + case loadAddress = "load_address" + case maxAddress = "max_address" + case name = "name" + case uuid = "uuid" + } + } + + /// The specific category of the error. It provides a high-level grouping for different types of errors. + public enum Category: String, Codable { + 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 + public struct Causes: Codable { + /// Error message + public var message: String + + /// Source of the error + public let source: Source + + /// Stacktrace of the error + public var stack: String? + + /// The type of the error + public let type: String? + + enum CodingKeys: String, CodingKey { + case message = "message" + case source = "source" + case stack = "stack" + case type = "type" + } + + /// Source of the error + public enum Source: String, Codable { + case network = "network" + case source = "source" + case console = "console" + case logger = "logger" + case agent = "agent" + case webview = "webview" + case custom = "custom" + case report = "report" + } + } + + /// Content Security Violation properties + public struct CSP: Codable { + /// In the context of CSP errors, indicates how the violated policy is configured to be treated by the user agent. + public let disposition: Disposition? + + enum CodingKeys: String, CodingKey { + case disposition = "disposition" + } + + /// In the context of CSP errors, indicates how the violated policy is configured to be treated by the user agent. + public enum Disposition: String, Codable { + case enforce = "enforce" + case report = "report" + } + } + + /// Whether the error has been handled manually in the source code or not + public enum Handling: String, Codable { + case handled = "handled" + case unhandled = "unhandled" + } + + /// Platform-specific metadata of the error event. + public struct Meta: Codable { + /// The CPU architecture of the process that crashed. + public let codeType: String? + + /// CPU specific information about the exception encoded into 64-bit hexadecimal number preceded by the signal code. + public let exceptionCodes: String? + + /// The name of the corresponding BSD termination signal. (in case of iOS crash) + public let exceptionType: String? + + /// A client-generated 16-byte UUID of the incident. + public let incidentIdentifier: String? + + /// Parent process information. + public let parentProcess: String? + + /// The location of the executable. + public let path: String? + + /// The name of the crashed process. + public let process: String? + + enum CodingKeys: String, CodingKey { + case codeType = "code_type" + case exceptionCodes = "exception_codes" + case exceptionType = "exception_type" + case incidentIdentifier = "incident_identifier" + case parentProcess = "parent_process" + case path = "path" + case process = "process" + } + } + + /// Resource properties of the error + public struct Resource: Codable { + /// HTTP method of the resource + public let method: RUMMethod + + /// The provider for this resource + public let provider: Provider? + + /// HTTP Status code of the resource + public let statusCode: Int64 + + /// URL of the resource + public var url: String + + enum CodingKeys: String, CodingKey { + case method = "method" + case provider = "provider" + case statusCode = "status_code" + case url = "url" + } + + /// The provider for this resource + public struct Provider: Codable { + /// The domain name of the provider + public let domain: String? + + /// The user friendly name of the provider + public let name: String? + + /// The type of provider + public let type: ProviderType? + + enum CodingKeys: String, CodingKey { + case domain = "domain" + case name = "name" + case type = "type" + } + + /// The type of provider + public enum ProviderType: String, Codable { + case ad = "ad" + case advertising = "advertising" + case analytics = "analytics" + case cdn = "cdn" + case content = "content" + case customerSuccess = "customer-success" + case firstParty = "first party" + case hosting = "hosting" + case marketing = "marketing" + case other = "other" + case social = "social" + case tagManager = "tag-manager" + case utility = "utility" + case video = "video" + } + } + } + + /// Source of the error + public enum Source: String, Codable { + case network = "network" + case source = "source" + case console = "console" + case logger = "logger" + case agent = "agent" + case webview = "webview" + case custom = "custom" + case report = "report" + } + + /// Source type of the error (the language or platform impacting the error stacktrace format) + public enum SourceType: String, Codable { + case android = "android" + case browser = "browser" + case ios = "ios" + case reactNative = "react-native" + case flutter = "flutter" + case roku = "roku" + case ndk = "ndk" + case iosIl2cpp = "ios+il2cpp" + case ndkIl2cpp = "ndk+il2cpp" + } + + /// Description of the thread in the process when error happened. + public struct Threads: Codable { + /// Tells if the thread crashed. + public let crashed: Bool + + /// Name of the thread (e.g. 'Thread 0'). + public let name: String + + /// Unsymbolicated stack trace of the given thread. + public let stack: String + + /// Platform-specific state of the thread when its state was captured (CPU registers dump for iOS, thread state enum for Android, etc.). + public let state: String? + + enum CodingKeys: String, CodingKey { + case crashed = "crashed" + case name = "name" + case stack = "stack" + case state = "state" + } + } + } + + /// Feature flags properties + public struct FeatureFlags: Codable { + public var featureFlagsInfo: [String: Encodable] + } + + /// Properties of App Hang and ANR errors + public struct Freeze: Codable { + /// Duration of the main thread freeze (in ns) + public let duration: Int64 + + enum CodingKeys: String, CodingKey { + case duration = "duration" + } + } + + /// Session properties + public struct Session: Codable { + /// Whether this session has a replay + public let hasReplay: Bool? + + /// UUID of the session + public let id: String + + /// Type of the session + public let type: RUMSessionType + + enum CodingKeys: String, CodingKey { + case hasReplay = "has_replay" + case id = "id" + case type = "type" + } + } + + /// The source of this event + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" + } + + /// View properties + public struct View: Codable { + /// UUID of the view + public let id: String + + /// Is the error starting in the foreground (focus in browser) + public let inForeground: Bool? + + /// User defined name of the view + public var name: String? + + /// URL that linked to the initial view of the page + public var referrer: String? + + /// URL of the view + public var url: String + + enum CodingKeys: String, CodingKey { + case id = "id" + case inForeground = "in_foreground" + case name = "name" + case referrer = "referrer" + case url = "url" + } + } +} + +extension RUMErrorEvent.FeatureFlags { + public func encode(to encoder: Encoder) throws { + // Encode dynamic properties: + var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) + try featureFlagsInfo.forEach { + let key = DynamicCodingKey($0) + try dynamicContainer.encode(AnyEncodable($1), forKey: key) + } + } + + public init(from decoder: Decoder) throws { + // Decode other properties into [String: Codable] dictionary: + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + let dynamicKeys = dynamicContainer.allKeys + var dictionary: [String: Codable] = [:] + + try dynamicKeys.forEach { codingKey in + dictionary[codingKey.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: codingKey) + } + + self.featureFlagsInfo = dictionary + } +} + +/// Schema of all properties of a Long Task event +public struct RUMLongTaskEvent: RUMDataModel { + /// Internal properties + public let dd: DD + + /// Action properties + public let action: Action? + + /// Application properties + public let application: Application + + /// Generated unique ID of the application build. Unlike version or build_version this field is not meant to be coming from the user, but rather generated by the tooling for each build. + public let buildId: String? + + /// The build version for this application + public let buildVersion: String? + + /// CI Visibility properties + public let ciTest: RUMCITest? + + /// Device connectivity properties + public let connectivity: RUMConnectivity? + + /// View Container properties (view wrapping the current view) + public let container: Container? + + /// User provided context + public var context: RUMEventAttributes? + + /// Start of the event in ms from epoch + public let date: Int64 + + /// Device properties + public let device: RUMDevice? + + /// Display properties + public let display: Display? + + /// Long Task properties + public let longTask: LongTask + + /// Operating system properties + public let os: RUMOperatingSystem? + + /// The service name for this application + public let service: String? + + /// Session properties + public let session: Session + + /// The source of this event + public let source: Source? + + /// Synthetics properties + public let synthetics: RUMSyntheticsTest? + + /// RUM event type + public let type: String = "long_task" + + /// User properties + public var usr: RUMUser? + + /// The version for this application + public let version: String? + + /// View properties + public var view: View + + enum CodingKeys: String, CodingKey { + case dd = "_dd" + case action = "action" + case application = "application" + case buildId = "build_id" + case buildVersion = "build_version" + case ciTest = "ci_test" + case connectivity = "connectivity" + case container = "container" + case context = "context" + case date = "date" + case device = "device" + case display = "display" + case longTask = "long_task" + case os = "os" + case service = "service" + case session = "session" + case source = "source" + case synthetics = "synthetics" + case type = "type" + case usr = "usr" + case version = "version" + case view = "view" + } + + /// Internal properties + public struct DD: Codable { + /// Browser SDK version + public let browserSdkVersion: String? + + /// Subset of the SDK configuration options in use during its execution + public let configuration: Configuration? + + /// Whether the long task should be discarded or indexed + public let discarded: Bool? + + /// Version of the RUM event format + public let formatVersion: Int64 = 2 + + /// Session-related internal properties + public let session: Session? + + enum CodingKeys: String, CodingKey { + case browserSdkVersion = "browser_sdk_version" + case configuration = "configuration" + case discarded = "discarded" + case formatVersion = "format_version" + case session = "session" + } + + /// Subset of the SDK configuration options in use during its execution + public struct Configuration: Codable { + /// The percentage of sessions with RUM & Session Replay pricing tracked + public let sessionReplaySampleRate: Double? + + /// The percentage of sessions tracked + public let sessionSampleRate: Double + + enum CodingKeys: String, CodingKey { + case sessionReplaySampleRate = "session_replay_sample_rate" + case sessionSampleRate = "session_sample_rate" + } + } + + /// Session-related internal properties + public struct Session: Codable { + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public let plan: Plan? + + /// The precondition that led to the creation of the session + public let sessionPrecondition: RUMSessionPrecondition? + + enum CodingKeys: String, CodingKey { + case plan = "plan" + case sessionPrecondition = "session_precondition" + } + + /// Session plan: 1 is the plan without replay, 2 is the plan with replay (deprecated) + public enum Plan: Int, Codable { + case plan1 = 1 + case plan2 = 2 + } + } + } + + /// Action properties + public struct Action: Codable { + /// UUID of the action + public let id: RUMActionID + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + + /// Application properties + public struct Application: Codable { + /// UUID of the application + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + + /// View Container properties (view wrapping the current view) + public struct Container: Codable { + /// Source of the parent view + public let source: Source + + /// Attributes of the view's container + public let view: View + + enum CodingKeys: String, CodingKey { + case source = "source" + case view = "view" + } + + /// Source of the parent view + public enum Source: String, Codable { + case android = "android" + case ios = "ios" + case browser = "browser" + case flutter = "flutter" + case reactNative = "react-native" + case roku = "roku" + case unity = "unity" + case kotlinMultiplatform = "kotlin-multiplatform" + } + + /// Attributes of the view's container + public struct View: Codable { + /// ID of the parent view + public let id: String + + enum CodingKeys: String, CodingKey { + case id = "id" + } + } + } + + /// Display properties + public struct Display: Codable { + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public let viewport: Viewport? + + enum CodingKeys: String, CodingKey { + case viewport = "viewport" + } + + /// The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + public struct Viewport: Codable { + /// Height of the viewport (in pixels) + public let height: Double + + /// Width of the viewport (in pixels) + public let width: Double + + enum CodingKeys: String, CodingKey { + case height = "height" + case width = "width" + } + } + } + + /// Long Task properties + public struct LongTask: Codable { + /// 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 + + /// 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